diff --git a/AGENTS.md b/AGENTS.md index 767be18f..e76647f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,12 +4,12 @@ - Follow `PROJECT_GOAL.md` and the user's current instructions. Work autonomously within scope. Resolve discoverable questions yourself; record, test, and revise reversible assumptions. Ask only when a consequential decision needs information or authorization not already available. Continue independent useful work while blocked elsewhere. - Map unfamiliar domains through primary sources, comparable systems, and real user workflows. Identify underlying needs, valuable concepts, and avoidable friction; evaluate improvements rather than merely reproducing features. Synthesize findings into decisions or experiments, and stop research when another action offers greater value. -- Make specifications precise enough for compatible independent implementations: definitions, relationships, invariants, valid/invalid examples, and lifecycle or transformation rules where relevant. Trace important requirements and decisions to implementation and verification. Resolve specification ambiguity at its source when implementation exposes it. +- Make specifications precise enough for compatible independent implementations: definitions, relationships, invariants, valid / invalid examples, and lifecycle or transformation rules where relevant. Trace important requirements and decisions to implementation and verification. Resolve specification ambiguity at its source when implementation exposes it. - For consequential uncertain choices, compare materially different candidates against explicit user-derived criteria. Ask independent critics to challenge assumptions and attempt to falsify proposals; refine or replace candidates as evidence warrants. Record the choice, evidence, tradeoffs, rejected approaches, and reasons to revisit it. Use proportionate effort for reversible local choices. - Plan milestones that resolve uncertainty or deliver useful capability, with explicit dependencies. Test foundational assumptions before substantial downstream work. At checkpoints, assess progress toward the actual objective and replan when evidence invalidates the strategy or progress stalls. - Define observable success and suitable evaluators before repeated improvement: invariants, examples, round trips, transformations, performance checks, realistic user tasks, or explicit review rubrics. Ensure the evaluator measures the intended outcome. For experiments, record the question, expectations, method, result, and implications; isolate exploratory prototypes from production architecture. -- Build and run meaningful changes, exercise actual behavior, and inspect useful logs. Investigate crashes, serious warnings, corrupted state, and interaction defects. For GUI work, use Computer Use or equivalent tools to inspect realistic workflows, cognitive load, editing/selection, navigation, feedback, recovery, loading/empty/failure states, accessibility, layout, focus/disabled states, and responsiveness. -- Match independent reviews to the artifact and risks. For languages or formats, include ambiguity, round trips, human/AI editability, and version-control behavior where relevant. Verify findings, fix the appropriate conceptual, specification, architectural, implementation, or interaction layer, and recheck affected dependents. Reuse valid evidence; repeat checks for concrete risks, changes, findings, or required gates. Compilation, screenshots, and worker reports alone do not establish correctness or usability. +- Build and run meaningful changes, exercise actual behavior, and inspect useful logs. Investigate crashes, serious warnings, corrupted state, and interaction defects. For GUI work, use Computer Use or equivalent tools to inspect realistic workflows, cognitive load, editing / selection, navigation, feedback, recovery, loading / empty / failure states, accessibility, layout, focus / disabled states, and responsiveness. +- Match independent reviews to the artifact and risks. For languages or formats, include ambiguity, round trips, human / AI editability, and version-control behavior where relevant. Verify findings, fix the appropriate conceptual, specification, architectural, implementation, or interaction layer, and recheck affected dependents. Reuse valid evidence; repeat checks for concrete risks, changes, findings, or required gates. Compilation, screenshots, and worker reports alone do not establish correctness or usability. - Favor the simplest maintainable design satisfying known requirements and justified extensibility. Avoid speculative abstraction, unrelated refactoring, and repeating approaches already invalidated by evidence. ## User input @@ -24,10 +24,10 @@ Keep one authoritative home for each kind of information. Use links and brief sc | Information | Authoritative home | | --- | --- | -| Live plans, priorities, dependencies, ownership, status, blockers, and handoffs | Gitea issues/milestones; linked PRs hold change review and validation. | -| Specifications, architecture, interfaces, and build/run documentation | Established version-controlled project documents. | +| Live plans, priorities, dependencies, ownership, status, blockers, and handoffs | Gitea issues / milestones; linked PRs hold change review and validation. | +| Specifications, architecture, interfaces, and build / run documentation | Established version-controlled project documents. | | Durable decisions and reusable evidence | `harness/DECISIONS.md` and relevant topic notes, or existing equivalents. | -| Navigation and integrated-state checkpoint | `harness/STATE.md`: links, verified code commit/release, capabilities, and material limitations. No live task ledger. | +| Navigation and integrated-state checkpoint | `harness/STATE.md`: links, verified code commit / release, capabilities, and material limitations. No live task ledger. | | Shared entrypoint | Gitea Wiki: overview and links, without duplicating versioned documentation. | | Published deliverables | Gitea releases, tied to source commits and verification. | @@ -38,9 +38,9 @@ Use Gitea for live coordination and the relevant code version for implementation All independent agents coordinating through Gitea run on completely separate machines, each with its own clone, filesystem, tools, processes, and runtime. Gitea and pushed Git history are the shared communication and delivery channels. Local files (including `harness`), unpushed commits, worktrees, test results, and localhost services are not accessible to peers. Matching branch names or paths do not imply synchronized contents. 1. On starting or resuming, including after compaction, inspect the working branch, local changes, applicable instructions, relevant shared records, and existing implementation. Recover context from authoritative records. Check for newer shared instructions on the actual remote default branch without overwriting local work. Read what is relevant; avoid repeated full-history scans and constant polling. -2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, remote repository/branch, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work. -3. Independent sessions are peers. Each owns its contribution and delegated work on its own machine. Record one integration/release owner when a milestone needs shared coordination. Use distinct remote work branches, partition files/interfaces, and coordinate changes to shared specifications and documents. Worktrees provide local isolation only; they do not share work between machines. -4. At meaningful checkpoints or handoffs, record changed results, commit/PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and label uncommitted or unpublished work as local-only. Before asking peers to consume results, push the required code and versioned documentation, including relevant `harness` updates; publish other required artifacts in Gitea and link them. Use repository-relative paths with commit references, reproducible commands, and necessary environment details; local paths and localhost URLs alone are not handoffs. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task. +2. Search before creating tasks. Claim a meaningful outcome in its Gitea issue with a stable session identity, scope, remote repository / branch, and acceptance criteria. Keep small steps within that task. Recheck after claiming; comments are not atomic locks. Respect existing ownership and resolve overlapping claims before overlapping work. +3. Independent sessions are peers. Each owns its contribution and delegated work on its own machine. Record one integration / release owner when a milestone needs shared coordination. Use distinct remote work branches, partition files / interfaces, and coordinate changes to shared specifications and documents. Worktrees provide local isolation only; they do not share work between machines. +4. At meaningful checkpoints or handoffs, record changed results, commit / PR links, verification commands and environment, blockers, and next actions. Identify the tested code version and label uncommitted or unpublished work as local-only. Before asking peers to consume results, push the required code and versioned documentation, including relevant `harness` updates; publish other required artifacts in Gitea and link them. Use repository-relative paths with commit references, reproducible commands, and necessary environment details; local paths and localhost URLs alone are not handoffs. Do not assume another agent is running or has read a message; silence alone does not establish abandonment. Record evidence and preserve existing work when reassigning a task. 5. Follow project review and merge conventions. Fetch the relevant remote branches and inspect the identified commits before reviewing or integrating another machine's work. Resolve conflicts while preserving others' edits, and verify the combined result locally; report material environment differences. Local instruction-file edits reach peers only after publication and retrieval. Do not overwrite others' work or force-push shared history to resolve coordination problems. 6. Close work when its acceptance criteria are met and the result is integrated or delivered as appropriate. Mark duplicates and superseded tasks explicitly with replacement links. Then choose further useful unowned work while the requested objective remains incomplete. @@ -64,10 +64,10 @@ Preserve concise records of decisions and rationale, useful experiments, failed Consolidate duplicates, repair stale guidance, and remove obsolete temporary files alongside useful project work. Before removing material, check references, active ownership, and ongoing use; preserve unique knowledge and update affected links. Prefer closing, superseding, or archiving history over deletion. User input is excluded from autonomous cleanup. -If legacy local task ledgers or plans duplicate Gitea, migrate their live content once into existing or appropriately scoped issues, preserve useful decisions/evidence, and mark the old records superseded. Record the migration so later sessions do not repeat it. Do not create empty documents or maintain records for ceremony. +If legacy local task ledgers or plans duplicate Gitea, migrate their live content once into existing or appropriately scoped issues, preserve useful decisions / evidence, and mark the old records superseded. Record the migration so later sessions do not repeat it. Do not create empty documents or maintain records for ceremony. ## Delivery -Publish versioned Gitea releases at meaningful working milestones; mark unfinished versions as prereleases. Check existing releases/tags and coordinate one owner and version before publishing. Build from an identified integrated commit and verify the packaged result and launch steps. +Publish versioned Gitea releases at meaningful working milestones; mark unfinished versions as prereleases. Check existing releases / tags and coordinate one owner and version before publishing. Build from an identified integrated commit and verify the packaged result and launch steps. -Include runnable builds or appropriate artifacts, concise release notes, build/run/deployment instructions as applicable, known limitations, tested platforms, and verification evidence. Confirm publication and artifact availability. Use existing authorized API access or project tooling when MCP lacks a needed release/upload operation. Do not silently replace published versions with different contents. +Include runnable builds or appropriate artifacts, concise release notes, build / run / deployment instructions as applicable, known limitations, tested platforms, and verification evidence. Confirm publication and artifact availability. Use existing authorized API access or project tooling when MCP lacks a needed release / upload operation. Do not silently replace published versions with different contents. diff --git a/PROJECT_GOAL.md b/PROJECT_GOAL.md index c3fa8f40..1326b089 100644 --- a/PROJECT_GOAL.md +++ b/PROJECT_GOAL.md @@ -17,14 +17,14 @@ Apply these criteria to the scope requested by the user: - Consequential decisions have evidence and rationale. Major assumptions have been validated, explicitly accepted, or isolated without compromising required behavior. - The result builds, runs, and operates on the required platforms. Relevant tests and evaluators pass; integrations, edge cases, and failure states have been exercised against the actual result. - User-facing behavior and usability have been exercised directly, using representative data and workflows. -- A fresh independent review covers requirements, domain/semantic consistency, usability, architecture, correctness, regressions, runtime behavior, documentation, and remaining risks. Valid material findings are addressed, blocking and high-impact issues resolved, and affected behavior reverified; no known blocker or high-severity defect remains. +- A fresh independent review covers requirements, domain / semantic consistency, usability, architecture, correctness, regressions, runtime behavior, documentation, and remaining risks. Valid material findings are addressed, blocking and high-impact issues resolved, and affected behavior reverified; no known blocker or high-severity defect remains. - Deliverables are maintainable, appropriately extensible, documented, and published as required by `AGENTS.md`. Obsolete scaffolding is removed; documentation and shared state accurately describe the result. Another engineer or agent can reproduce the verification and continue the work. If required work or verification is blocked, record the specific gap and continue independent useful work without claiming completion. ## Final handoff -Link the delivered result and summarize capabilities, discoveries, architecture and decisions, relevant rejected approaches, verification and independent review, remaining limitations, and exact build/run/deployment/verification instructions. Refer to authoritative records instead of repeating the project history. +Link the delivered result and summarize capabilities, discoveries, architecture and decisions, relevant rejected approaches, verification and independent review, remaining limitations, and exact build / run / deployment / verification instructions. Refer to authoritative records instead of repeating the project history. ## USER-OWNED APPLICATION SPECIFICATION diff --git a/README.md b/README.md index 5cc5dc70..1823acd6 100644 --- a/README.md +++ b/README.md @@ -94,11 +94,13 @@ Create named **Exchange categories** at the perspective root to group Work excha In **Select / move**, drag cards on a 10-unit grid, drag empty space to pan, and scroll to zoom. Drag ports along their owner's border or onto any of its four sides. Automatic sides account for direction and visible connections. Manual positions are per diagram and remain relative to the owner through movement or resize; right-click **Reset automatic position** restores automatic placement. **Arrange** changes the current view layout; **Fit** frames its projected bounds. SVG exports the current architecture diagram as a view, not an editable model file. +In saved architecture and State views, **Shift+click** adds or removes an exact displayed item; its context menu also offers **Add to selection** and **Remove from selection**. Properties edits only the primary item. Drag a selected member to move a placement-only group in one operation, including visual descendants once and the authored pins of connections whose endpoint owners move together. A stationary click selects only that member; Escape during a drag cancels it and keeps the group. **Hide selected** removes only selected placements, preserving model elements, unselected descendant placements and other views; one Undo restores the operation. Groups containing connections, ports or State action/internal-transition rows cannot be moved or hidden as placement groups. State rows select their relation, never their owning placement. Dragging a group of allocated work cards and deleting a group from the model remain unavailable; these actions refuse the whole group instead of editing just its primary item. Selection itself is temporary and is not saved. + Connections start with orthogonal routes that avoid cards and container headers. In Select mode, select a connection and drag a segment to add a bend, or right-click for **Add bend here**. Drag its circles to adjust the route; the menu offers **Remove bend** and **Reset routing**. With the canvas focused, Insert adds a bend and Delete removes the selected bend. Shift+F10 opens the selected item's menu; **Actions** in Properties provides the same commands. Drag the connection label independently. Each completed gesture is one undoable edit; Escape cancels a drag. Routes belong to the current diagram and persist in source, JSON, REST and SVG export. Folding an endpoint container temporarily uses an automatic route; expanding it restores your authored route. Dense diagrams can still need manual adjustment; automatic routing is bounded and does not guarantee an overlap-free result. For state diagrams, use **+ Region** inside a highlighted machine or State/Mode, then choose **+ State / symbol** and click its owning region. **Transition** supports dragging between compatible vertices or clicking the source and then the target. Select a drawn transition and drag its endpoint handle, or right-click **Reconnect source/target**. Right-click a state or the background for **New transition with details…** to create Local, Internal or timed transitions; an existing transition's **Edit transition** form edits its kind, triggers, guard and effects. EntryPoint outgoing transitions use Local. -In the state view, **Select / move** drags vertices and containers without reparenting; container movement includes its descendants. Middle-button drag pans, scroll or **−/+** zooms, and **Fit** frames the view. Right-click for **Fold contents**, **Unfold contents**, **Placement properties**, or **Hide from this diagram**. The **Show an element not yet in this view…** selector restores missing placements. Escape cancels a gesture; **Undo** restores a completed edit. **+ View** creates an independent layout. The persisted diagram's machine reference and placements are the same records in source, JSON and REST; opening the page alone creates no diagram. +In the state view, **Select / move** drags vertices and containers without reparenting or grid snapping; container movement includes its descendants. Multiple selected placements share the same fractional displacement, including authored positions of boundary symbols. State route bends retain their separate 5-unit grid. Middle-button drag pans, scroll or **−/+** zooms, and **Fit** frames the view. Right-click for **Fold contents**, **Unfold contents**, **Placement properties**, or **Hide from this diagram**. The **Show an element not yet in this view…** selector restores missing placements. Escape cancels a gesture; **Undo** restores a completed edit. **+ View** creates an independent layout. The persisted diagram's machine reference and placements are the same records in source, JSON and REST; opening the page alone creates no diagram. ## Edit the same model as text diff --git a/crates/oa-core/examples/multiselect_native.rs b/crates/oa-core/examples/multiselect_native.rs new file mode 100644 index 00000000..880864b2 --- /dev/null +++ b/crates/oa-core/examples/multiselect_native.rs @@ -0,0 +1,717 @@ +//! Native evidence fixture/checker; install as a temporary oa-core example. +//! This invariant evaluator derives delta from saved A, NOT from pointer input. +use oa_core::{source_project::*, *}; +use std::{fs, io::Write, path::Path}; +type Result = std::result::Result>; + +fn id(alias: &str) -> Id { + // Make the intended OA view first for default UUID-ordered view selection. + if alias == "oa_view" { + return Id::from_u128(0x29_0001); + } + Id::new_v5( + &Id::from_u128(0xc1192026_0923_5000_8000_000000000029), + alias.as_bytes(), + ) +} +fn point(x: f32, y: f32) -> Position { + Position { x, y } +} +fn element(p: &mut Project, alias: &str, name: &str, kind: Kind, layer: Layer, parent: Option) { + p.elements.insert( + id(alias), + Element { + id: id(alias), + alias: alias.into(), + name: name.into(), + description: format!("Preserve engineering record {alias}."), + parent, + body: ElementBody::new(kind, layer).unwrap(), + }, + ); +} +fn diagram(p: &mut Project, alias: &str, name: &str, layer: Layer) { + p.diagrams.insert( + id(alias), + Diagram { + id: id(alias), + alias: alias.into(), + name: name.into(), + description: "Selection changes placement geometry only.".into(), + layer, + projection: ProjectionKind::Overview, + view_options: Default::default(), + layout: None, + nodes: Default::default(), + routes: vec![], + }, + ); +} +fn place(p: &mut Project, view: &str, alias: &str, object: &str, position: Position, size: Size) { + p.diagrams.get_mut(&id(view)).unwrap().nodes.insert( + id(alias), + NodePlacement { + id: id(alias), + alias: alias.into(), + element: id(object), + position, + size, + collapsed: false, + port_anchors: vec![], + }, + ); +} +fn route(edge: &str, pins: Vec) -> EdgeRoute { + EdgeRoute { + edge: id(edge), + waypoints: pins, + source_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Right, + offset: 0.25, + }), + target_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Left, + offset: 0.7, + }), + label: Some(EdgeLabel { + fraction: 0.45, + offset: point(7., -19.), + }), + } +} +fn fixture() -> Result { + let mut p = Project::empty(); + p.id = id("project"); + p.alias = "multiselect_native".into(); + p.name = "Multiselect native".into(); + p.description = + "Shift-select A and B; move together, retain C and engineering semantics.".into(); + for (alias, name, parent) in [ + ("activity_a", "A - receive request", None), + ("activity_b", "B - assess request", None), + ("activity_c", "C - dispatch response", None), + ( + "activity_child", + "A.1 - classify request", + Some(id("activity_a")), + ), + ] { + element(&mut p, alias, name, Kind::Activity, Layer::Oa, parent); + } + element( + &mut p, + "sa_function", + "Technical request processing", + Kind::Function, + Layer::Sa, + None, + ); + for (alias, name, source, target) in [ + ("internal", "Request assessment", "activity_a", "activity_b"), + ("external", "Response decision", "activity_b", "activity_c"), + ] { + let old = p.relate(RelationKind::Interaction, name, id(source), id(target)); + reidentify(&mut p, old, id(alias))?; + p.relations.get_mut(&id(alias)).unwrap().alias = alias.into(); + } + diagram( + &mut p, + "oa_view", + "Multiselect native - OA overview", + Layer::Oa, + ); + diagram(&mut p, "oa_other", "Other OA view - preserve", Layer::Oa); + diagram(&mut p, "sa_view", "SA engineering - preserve", Layer::Sa); + let size = Size { + width: 224., + height: 94., + }; + place( + &mut p, + "oa_view", + "node_a", + "activity_a", + point(80., 80.), + Size { + width: 310., + height: 260., + }, + ); + place( + &mut p, + "oa_view", + "node_b", + "activity_b", + point(470., 120.), + size, + ); + place( + &mut p, + "oa_view", + "node_c", + "activity_c", + point(780., 270.), + size, + ); + place( + &mut p, + "oa_view", + "node_child", + "activity_child", + point(120., 210.), + size, + ); + place( + &mut p, + "oa_other", + "other_a", + "activity_a", + point(40., 60.), + size, + ); + place( + &mut p, + "sa_view", + "sa_node", + "sa_function", + point(100., 110.), + size, + ); + p.diagrams.get_mut(&id("oa_view")).unwrap().routes = vec![ + route("internal", vec![point(420., 145.), point(420., 186.)]), + route("external", vec![point(725., 150.), point(725., 336.)]), + ]; + p.traces.insert( + id("trace"), + Trace { + id: id("trace"), + alias: "preserved_refinement".into(), + name: "Reviewed request refinement".into(), + description: "Group geometry and Hide preserve this trace and both endpoints.".into(), + oa: id("activity_a"), + sa: id("sa_function"), + kind: TraceKind::Refinement, + baseline: Some(TraceBaseline { + oa: p.properties(id("activity_a")).unwrap(), + sa: p.properties(id("sa_function")).unwrap(), + semantic: None, + }), + }, + ); + validate_candidate(&p)?; + Ok(p) +} +fn load(path: &Path) -> Result { + if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia")) + { + let disk = source_files::load_project(path, ProjectLimits::default())?; + // This deliberately partial engineering fixture has completeness + // warnings (allocation/behavior context). They are not source errors. + // Compilation, candidate validation and whole-Project equality still + // reject malformed source or any unexpected model change. + if disk + .diagnostics + .iter() + .any(|d| d.diagnostic.severity == validation::Severity::Error) + { + return Err(format!("Source discovery diagnostics: {:?}", disk.diagnostics).into()); + } + let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()) + .map_err(|e| format!("Source compile diagnostics: {e:?}"))?; + Ok(compiled.decoded.project) + } else { + Ok(persistence::load(path)?) + } +} +fn equal(expected: &Project, actual: &Project) -> Result<()> { + if expected == actual { + return Ok(()); + } + let e = serde_json::to_value(expected)?; + let a = serde_json::to_value(actual)?; + let fields: Vec<_> = e + .as_object() + .unwrap() + .keys() + .filter(|key| e[*key] != a[*key]) + .cloned() + .collect(); + Err(format!("Whole Project mismatch in {fields:?}; no partial PASS").into()) +} +fn roundtrip(p: &Project) -> Result { + validate_candidate(p)?; + equal(p, &persistence::decode(&serde_json::to_vec(p)?)?)?; + let bundle = format_bundle(p); + let compiled = compile_bundle(&bundle, ProjectLimits::default()) + .map_err(|e| format!("Roundtrip: {e:?}"))?; + equal(p, &compiled.decoded.project)?; + Ok(bundle) +} +fn write_new(path: &Path, bytes: &[u8]) -> Result<()> { + fs::create_dir_all(path.parent().ok_or("Missing parent directory")?)?; + fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)? + .write_all(bytes)?; + Ok(()) +} +fn generate(out: &Path) -> Result<()> { + if out.exists() { + return Err("Output directory already exists; choose a fresh path".into()); + } + let p = fixture()?; + let bundle = roundtrip(&p)?; + write_new(&out.join("baseline.json"), &serde_json::to_vec_pretty(&p)?)?; + for (path, source) in &bundle.files { + write_new(&out.join("source").join(path), source.as_bytes())?; + } + let names = [ + "project", + "oa_view", + "oa_other", + "sa_view", + "activity_a", + "activity_b", + "activity_c", + "activity_child", + "node_a", + "node_b", + "node_c", + "node_child", + "internal", + "external", + "trace", + "sa_function", + "sa_node", + "other_a", + ]; + let ids: std::collections::BTreeMap<_, _> = + names.into_iter().map(|name| (name, id(name))).collect(); + write_new( + &out.join("manifest.json"), + &serde_json::to_vec_pretty(&serde_json::json!({ + "fixture": "multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry, + "move_members": ["node_a", "node_b", "node_child"], "fixed_member": "node_c", + "evaluation": "Whole Project invariant comparison; saved A supplies bounded shared delta, not an exact pointer oracle", + "hide": "Remove node_a/node_b only; retain child, stored routes, all model records and other views", + }))?, + )?; + equal(&p, &load(&out.join("baseline.json"))?)?; + equal(&p, &load(&out.join("source/project.arcadia"))?)?; + println!("PASS generated JSON and source bundle: {}", out.display()); + Ok(()) +} +fn check(mode: &str, baseline: &Path, saved: &Path) -> Result<()> { + let before = load(baseline)?; + let actual = load(saved)?; + validate_candidate(&before)?; + validate_candidate(&actual)?; + if before.id != id("project") { + return Err("Not this fixture's baseline project".into()); + } + let mut expected = before.clone(); + let mut delta = None; + match mode { + "unchanged" => {} + "hide" => { + let view = expected + .diagrams + .get_mut(&id("oa_view")) + .ok_or("Missing OA view")?; + for alias in ["node_a", "node_b"] { + view.nodes + .remove(&id(alias)) + .ok_or("Baseline selected placement missing")?; + } + // references::remove_objects removes routes only when their semantic + // edge is deleted. Hide deletes placements; both routes MUST survive. + } + "moved" => { + let old = before + .diagrams + .get(&id("oa_view")) + .ok_or("Missing baseline OA view")?; + let new = actual + .diagrams + .get(&id("oa_view")) + .ok_or("Missing saved OA view")?; + let a = old + .nodes + .get(&id("node_a")) + .ok_or("Missing baseline A")? + .position; + let moved_a = new + .nodes + .get(&id("node_a")) + .ok_or("Missing saved A")? + .position; + let d = point(moved_a.x - a.x, moved_a.y - a.y); + if !d.x.is_finite() + || !d.y.is_finite() + || d.x.abs().max(d.y.abs()) > 500. + || d.x.hypot(d.y) < 10. + { + return Err(format!( + "Move delta must have length >=10 and each axis <=500 world units: {d:?}" + ) + .into()); + } + if moved_a.x % 10. != 0. || moved_a.y % 10. != 0. { + return Err("Saved anchor is not on 10-unit grid".into()); + } + let view = expected.diagrams.get_mut(&id("oa_view")).unwrap(); + // Independent explicit fixture enumeration: no group/descendant/ + // routing planner is called to predict the result. + for alias in ["node_a", "node_b", "node_child"] { + let node = view + .nodes + .get_mut(&id(alias)) + .ok_or("Missing expected moved placement")?; + node.position.x += d.x; + node.position.y += d.y; + } + let internal = view + .routes + .iter_mut() + .find(|route| route.edge == id("internal")) + .ok_or("Missing internal pins")?; + for pin in &mut internal.waypoints { + pin.x += d.x; + pin.y += d.y; + } + delta = Some(d); + } + _ => return Err("Mode must be moved, hide, or unchanged".into()), + } + equal(&expected, &actual)?; + roundtrip(&actual)?; + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "result": "PASS", "mode": mode, "baseline": baseline, "saved": saved, + "comparison": "complete Project equality", "inferred_common_delta": delta, + "limits": "Invariant evaluator only; no pointer/action/focus/Undo/source-byte acceptance claim", + }))? + ); + Ok(()) +} +fn main() -> Result<()> { + let args: Vec<_> = std::env::args().skip(1).collect(); + match args.as_slice() { + [command, out] if command == "generate" => generate(Path::new(out)), + [command, mode, baseline, saved] if command == "check" => check(mode, Path::new(baseline), Path::new(saved)), + _ => Err("Usage: multiselect-native-producer generate DIRECTORY | check moved|hide|unchanged BASELINE SAVED".into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + // Produce evidence through real validated commands; the checker itself uses + // explicit fixture enumeration and never calls this movement implementation. + fn moved(before: &Project, delta: Position) -> Project { + let command = diagram_edit::translate_placements( + before, + id("oa_view"), + &BTreeSet::from([id("node_a"), id("node_b")]), + delta, + ) + .unwrap(); + let mut session = Session::new(before.clone()); + session.execute(command).unwrap(); + session.project().clone() + } + fn hidden(before: &Project) -> Project { + let mut session = Session::new(before.clone()); + session + .execute(Command::Batch(vec![ + Command::Delete(id("node_a")), + Command::Delete(id("node_b")), + ])) + .unwrap(); + session.project().clone() + } + fn through_saved_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> { + let dir = tempfile::tempdir()?; + let before = dir.path().join("before.json"); + let saved = dir.path().join("saved.json"); + // These assertions distinguish meaningful invariant corruptions from + // malformed projects rejected before the evaluator even sees them. + validate_candidate(baseline).expect("test baseline must be valid"); + validate_candidate(actual).expect("corruption must remain a valid model"); + persistence::save(&before, baseline)?; + persistence::save(&saved, actual)?; + check(mode, &before, &saved) + } + fn mismatch(mode: &str, before: &Project, actual: &Project, case: &str) { + let error = through_saved_json(mode, before, actual) + .expect_err(case) + .to_string(); + assert!( + error.contains("Whole Project mismatch"), + "{case}: unexpected rejection: {error}" + ); + } + fn edge<'a>(p: &'a mut Project, alias: &str) -> &'a mut EdgeRoute { + p.diagrams + .get_mut(&id("oa_view")) + .unwrap() + .routes + .iter_mut() + .find(|route| route.edge == id(alias)) + .unwrap() + } + + #[test] + fn generated_json_and_real_source_bundle_are_accepted_unchanged() { + let dir = tempfile::tempdir().unwrap(); + let out = dir.path().join("fixture"); + generate(&out).unwrap(); + let baseline = out.join("baseline.json"); + let saved = out.join("saved.json"); + persistence::save(&saved, &fixture().unwrap()).unwrap(); + check("unchanged", &baseline, &saved).unwrap(); + check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap(); + let bytes = fs::read(&baseline).unwrap(); + assert!( + generate(&out).is_err(), + "generation cannot overwrite evidence" + ); + assert_eq!(fs::read(&baseline).unwrap(), bytes); + let invalid = dir.path().join("invalid.arcadia"); + fs::write(&invalid, "project {").unwrap(); + assert!(load(&invalid).is_err(), "source errors must still reject"); + } + + #[test] + fn real_group_command_outputs_pass_and_are_not_unchanged() { + let before = fixture().unwrap(); + for delta in [point(40., 60.), point(-20., 30.)] { + let actual = moved(&before, delta); + through_saved_json("moved", &before, &actual).unwrap(); + mismatch( + "unchanged", + &before, + &actual, + "real movement cannot count as unchanged", + ); + } + } + + #[test] + fn real_hide_command_outputs_pass_before_and_after_movement() { + let initial = fixture().unwrap(); + for before in [initial.clone(), moved(&initial, point(40., 60.))] { + let actual = hidden(&before); + through_saved_json("hide", &before, &actual).unwrap(); + mismatch( + "unchanged", + &before, + &actual, + "real Hide cannot count as unchanged", + ); + } + } + + #[test] + fn omitted_and_double_moved_child_are_rejected() { + let before = fixture().unwrap(); + let delta = point(40., 60.); + let correct = moved(&before, delta); + let initial = before.diagrams[&id("oa_view")].nodes[&id("node_child")].position; + for position in [ + initial, + point(initial.x + 2. * delta.x, initial.y + 2. * delta.y), + ] { + let mut bad = correct.clone(); + bad.diagrams + .get_mut(&id("oa_view")) + .unwrap() + .nodes + .get_mut(&id("node_child")) + .unwrap() + .position = position; + mismatch("moved", &before, &bad, "child did not move exactly once"); + } + } + + #[test] + fn internal_pins_left_behind_and_external_pins_moved_are_rejected() { + let mut before = fixture().unwrap(); + let original_pins = edge(&mut before, "internal").waypoints.clone(); + let correct = moved(&before, point(40., 60.)); + let mut bad = correct.clone(); + edge(&mut bad, "internal").waypoints = original_pins; + mismatch("moved", &before, &bad, "internal pins left behind"); + let mut bad = correct; + for pin in &mut edge(&mut bad, "external").waypoints { + pin.x += 40.; + pin.y += 60.; + } + mismatch("moved", &before, &bad, "one-ended pins moved"); + } + + #[test] + fn manual_attachments_and_label_data_must_remain_exact() { + let before = fixture().unwrap(); + let correct = moved(&before, point(40., 60.)); + let mutations: [fn(&mut EdgeRoute); 4] = [ + |r| r.source_attachment.as_mut().unwrap().offset = 0.65, + |r| r.target_attachment.as_mut().unwrap().side = PortAnchorSide::Top, + |r| r.label.as_mut().unwrap().offset.x += 40., + |r| r.label.as_mut().unwrap().fraction = 0.75, + ]; + for mutate in mutations { + let mut bad = correct.clone(); + mutate(edge(&mut bad, "internal")); + mismatch("moved", &before, &bad, "relative route metadata changed"); + } + } + + #[test] + fn semantic_other_view_fixed_node_and_trace_drift_are_rejected() { + let before = fixture().unwrap(); + let correct = moved(&before, point(40., 60.)); + let mutations: [fn(&mut Project); 5] = [ + |p| { + p.elements + .get_mut(&id("activity_a")) + .unwrap() + .description + .push_str(" drift") + }, + |p| { + p.diagrams + .get_mut(&id("oa_other")) + .unwrap() + .nodes + .get_mut(&id("other_a")) + .unwrap() + .position + .x += 40. + }, + |p| { + p.diagrams + .get_mut(&id("sa_view")) + .unwrap() + .nodes + .get_mut(&id("sa_node")) + .unwrap() + .position + .y += 60. + }, + |p| { + p.diagrams + .get_mut(&id("oa_view")) + .unwrap() + .nodes + .get_mut(&id("node_c")) + .unwrap() + .position + .x += 40. + }, + |p| { + p.traces + .get_mut(&id("trace")) + .unwrap() + .description + .push_str(" drift") + }, + ]; + for mutate in mutations { + let mut bad = correct.clone(); + mutate(&mut bad); + mismatch( + "moved", + &before, + &bad, + "record outside permitted movement changed", + ); + } + } + + #[test] + fn zero_small_excessive_and_offgrid_moves_cannot_satisfy_move_evidence() { + let before = fixture().unwrap(); + for (delta, reason) in [ + (point(0., 0.), "length >=10"), + (point(3., 0.), "length >=10"), + (point(510., 0.), "each axis <=500"), + (point(20.5, 0.), "10-unit grid"), + ] { + let actual = moved(&before, delta); + let error = through_saved_json("moved", &before, &actual) + .unwrap_err() + .to_string(); + assert!(error.contains(reason), "{delta:?}: {error}"); + } + } + + #[test] + fn hide_must_preserve_child_routes_and_semantic_objects() { + let before = fixture().unwrap(); + let correct = hidden(&before); + let mut missing_child = correct.clone(); + missing_child + .diagrams + .get_mut(&id("oa_view")) + .unwrap() + .nodes + .remove(&id("node_child")); + mismatch( + "hide", + &before, + &missing_child, + "Hide removed unselected child placement", + ); + let mut missing_routes = correct; + missing_routes + .diagrams + .get_mut(&id("oa_view")) + .unwrap() + .routes + .clear(); + mismatch( + "hide", + &before, + &missing_routes, + "Hide removed persistent route records", + ); + let mut semantic_delete = Session::new(before.clone()); + semantic_delete + .execute(Command::Batch(vec![ + Command::Delete(id("activity_a")), + Command::Delete(id("activity_b")), + ])) + .unwrap(); + mismatch( + "hide", + &before, + semantic_delete.project(), + "Hide became semantic deletion", + ); + } + + #[test] + fn unchanged_rejects_valid_semantic_drift_without_geometry_changes() { + let before = fixture().unwrap(); + let mut actual = before.clone(); + actual + .elements + .get_mut(&id("activity_b")) + .unwrap() + .name + .push_str(" edited"); + mismatch( + "unchanged", + &before, + &actual, + "geometry equality is not complete Project equality", + ); + } +} diff --git a/crates/oa-core/examples/state_multiselect_native.rs b/crates/oa-core/examples/state_multiselect_native.rs new file mode 100644 index 00000000..77a40187 --- /dev/null +++ b/crates/oa-core/examples/state_multiselect_native.rs @@ -0,0 +1,912 @@ +//! Deterministic persisted-State native fixture and complete-Project checker. +//! This invariant evaluator derives delta from saved A, NOT from pointer input. +use oa_core::{source_project::*, *}; +use std::{fs, io::Write, path::Path}; +type Result = std::result::Result>; + +fn id(alias: &str) -> Id { + // Dedicated State view is the smallest diagram UUID, for native opening. + if alias == "state_view" { + return Id::from_u128(0x29_0101); + } + Id::new_v5( + &Id::from_u128(0xc1192026_0923_5000_8000_000000002929), + alias.as_bytes(), + ) +} +fn point(x: f32, y: f32) -> Position { + Position { x, y } +} +fn element(p: &mut Project, alias: &str, name: &str, kind: Kind, layer: Layer, parent: Option) { + p.elements.insert( + id(alias), + Element { + id: id(alias), + alias: alias.into(), + name: name.into(), + description: format!("Preserve engineering record {alias}."), + parent, + body: ElementBody::new(kind, layer).unwrap(), + }, + ); +} +fn diagram(p: &mut Project, alias: &str, name: &str, layer: Layer) { + p.diagrams.insert( + id(alias), + Diagram { + id: id(alias), + alias: alias.into(), + name: name.into(), + description: "Selection changes placement geometry only.".into(), + layer, + projection: ProjectionKind::Overview, + view_options: Default::default(), + layout: None, + nodes: Default::default(), + routes: vec![], + }, + ); +} +fn place(p: &mut Project, view: &str, alias: &str, object: &str, position: Position, size: Size) { + p.diagrams.get_mut(&id(view)).unwrap().nodes.insert( + id(alias), + NodePlacement { + id: id(alias), + alias: alias.into(), + element: id(object), + position, + size, + collapsed: false, + port_anchors: vec![], + }, + ); +} +fn route(edge: &str, pins: Vec) -> EdgeRoute { + EdgeRoute { + edge: id(edge), + waypoints: pins, + source_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Right, + offset: 0.25, + }), + target_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Left, + offset: 0.7, + }), + label: Some(EdgeLabel { + fraction: 0.45, + offset: point(7., -19.), + }), + } +} +fn transition( + p: &mut Project, + alias: &str, + name: &str, + source: &str, + target: &str, + kind: TransitionKind, +) -> Result<()> { + let old = p.relate(RelationKind::StateTransition, name, id(source), id(target)); + reidentify(p, old, id(alias))?; + let relation = p.relations.get_mut(&id(alias)).unwrap(); + relation.alias = alias.into(); + relation.description = format!("Preserve transition {alias}."); + if let RelationBody::StateTransition { + transition_kind, + guard, + .. + } = &mut relation.body + { + *transition_kind = kind; + *guard = if alias == "between" { + "request_ready" + } else { + "" + } + .into(); + } + Ok(()) +} +fn fixture() -> Result { + let mut p = Project::empty(); + p.id = id("project"); + p.alias = "state_multiselect_native".into(); + p.name = "State group native".into(); + p.description = "Shift-select A and B; move their placements together. C and shared engineering records stay fixed.".into(); + for (alias, name, kind, parent) in [ + ("crew", "Operations crew", Kind::Entity, None), + ( + "machine", + "Request lifecycle", + Kind::StateMachine, + Some(id("crew")), + ), + ("region", "Main region", Kind::Region, Some(id("machine"))), + ("state_a", "A - Processing", Kind::State, Some(id("region"))), + ("state_b", "B - Ready", Kind::State, Some(id("region"))), + ("state_c", "C - Complete", Kind::State, Some(id("region"))), + ( + "inner", + "A nested region", + Kind::Region, + Some(id("state_a")), + ), + ( + "state_leaf", + "A.1 - Classify", + Kind::State, + Some(id("inner")), + ), + ("entry", "Enter A", Kind::EntryPoint, Some(id("inner"))), + ("exit", "Leave A", Kind::ExitPoint, Some(id("inner"))), + ("shared_work", "Record request", Kind::Activity, None), + ] { + element(&mut p, alias, name, kind, Layer::Oa, parent); + } + element( + &mut p, + "sa_function", + "Technical record request", + Kind::Function, + Layer::Sa, + None, + ); + for (alias, name, source, target, kind) in [ + ( + "between", + "Ready\nfor review", + "state_a", + "state_b", + TransitionKind::External, + ), + ( + "external", + "Complete request", + "state_b", + "state_c", + TransitionKind::External, + ), + ( + "entry_route", + "Begin classification", + "entry", + "state_leaf", + TransitionKind::Local, + ), + ( + "exit_route", + "Classification complete", + "state_leaf", + "exit", + TransitionKind::External, + ), + ( + "refresh", + "Refresh record", + "state_a", + "state_a", + TransitionKind::Internal, + ), + ] { + transition(&mut p, alias, name, source, target, kind)?; + } + for (alias, source, phase) in [ + ("action_a", "state_a", StateActionKind::Entry), + ("action_c", "state_c", StateActionKind::Do), + ] { + let old = p.relate( + RelationKind::StateAction, + "Record shared work", + id(source), + id("shared_work"), + ); + reidentify(&mut p, old, id(alias))?; + let relation = p.relations.get_mut(&id(alias)).unwrap(); + relation.alias = alias.into(); + relation.description = format!("Preserve shared-work action {alias}."); + if let RelationBody::StateAction { phase: value, .. } = &mut relation.body { + *value = phase; + } + } + for (alias, name) in [ + ("state_view", "State multiselect - main"), + ("state_other", "Other State view - preserve"), + ] { + diagram(&mut p, alias, name, Layer::Oa); + p.diagrams.get_mut(&id(alias)).unwrap().projection = ProjectionKind::StateMachine { + machine: id("machine"), + }; + } + diagram(&mut p, "sa_view", "SA engineering - preserve", Layer::Sa); + // The main machine and outer Region are deliberately unplaced to keep + // the target headers readable in a 1442 x 932 native window after Fit. + // A starts at the authored origin so saved A recovers the exact f32 delta; + // subtracting a nonzero original can round it differently from sibling XY. + for (alias, object, x, y, width, height) in [ + ("node_a", "state_a", 0., 0., 340., 450.), + ("node_b", "state_b", 410., 30., 210., 150.), + ("node_c", "state_c", 410., 330., 210., 140.), + ("node_inner", "inner", 25., 220., 290., 200.), + ("node_leaf", "state_leaf", 60., 290., 180., 90.), + ("node_entry", "entry", -210., 280., 24., 24.), + ("node_exit", "exit", 870., 350., 24., 24.), + ] { + place( + &mut p, + "state_view", + alias, + object, + point(x, y), + Size { width, height }, + ); + } + let size = Size { + width: 224., + height: 94., + }; + place( + &mut p, + "state_other", + "other_a", + "state_a", + point(40., 60.), + Size { + width: 340., + height: 220., + }, + ); + place( + &mut p, + "state_other", + "other_b", + "state_b", + point(460., 80.), + size, + ); + place( + &mut p, + "sa_view", + "sa_node", + "sa_function", + point(100., 110.), + size, + ); + p.diagrams.get_mut(&id("state_view")).unwrap().routes = vec![ + route("between", vec![point(365., 70.), point(365., 120.)]), + route("external", vec![point(660., 140.), point(660., 360.)]), + route("entry_route", vec![point(40., 280.), point(40., 320.)]), + route("exit_route", vec![point(290., 335.), point(290., 370.)]), + ]; + p.traces.insert( + id("trace"), + Trace { + id: id("trace"), + alias: "preserved_refinement".into(), + name: "Reviewed recording refinement".into(), + description: + "Moving states or hiding placements must preserve shared work and its refinement." + .into(), + oa: id("shared_work"), + sa: id("sa_function"), + kind: TraceKind::Refinement, + baseline: Some(TraceBaseline { + oa: p.properties(id("shared_work")).unwrap(), + sa: p.properties(id("sa_function")).unwrap(), + semantic: None, + }), + }, + ); + validate_candidate(&p)?; + Ok(p) +} +fn load(path: &Path) -> Result { + if path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia")) + { + let disk = source_files::load_project(path, ProjectLimits::default())?; + // This deliberately partial engineering fixture has completeness + // warnings (allocation/behavior context). They are not source errors. + // Compilation, candidate validation and whole-Project equality still + // reject malformed source or any unexpected model change. + if disk + .diagnostics + .iter() + .any(|d| d.diagnostic.severity == validation::Severity::Error) + { + return Err(format!("Source discovery diagnostics: {:?}", disk.diagnostics).into()); + } + let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()) + .map_err(|e| format!("Source compile diagnostics: {e:?}"))?; + Ok(compiled.decoded.project) + } else { + Ok(persistence::load(path)?) + } +} +fn equal(expected: &Project, actual: &Project) -> Result<()> { + if expected == actual { + return Ok(()); + } + let e = serde_json::to_value(expected)?; + let a = serde_json::to_value(actual)?; + let fields: Vec<_> = e + .as_object() + .unwrap() + .keys() + .filter(|key| e[*key] != a[*key]) + .cloned() + .collect(); + Err(format!("Whole Project mismatch in {fields:?}; no partial PASS").into()) +} +fn roundtrip(p: &Project) -> Result { + validate_candidate(p)?; + equal(p, &persistence::decode(&serde_json::to_vec(p)?)?)?; + let bundle = format_bundle(p); + let compiled = compile_bundle(&bundle, ProjectLimits::default()) + .map_err(|e| format!("Roundtrip: {e:?}"))?; + equal(p, &compiled.decoded.project)?; + Ok(bundle) +} +fn write_new(path: &Path, bytes: &[u8]) -> Result<()> { + fs::create_dir_all(path.parent().ok_or("Missing parent directory")?)?; + fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)? + .write_all(bytes)?; + Ok(()) +} +fn generate(out: &Path) -> Result<()> { + if out.exists() { + return Err("Output directory already exists; choose a fresh path".into()); + } + let p = fixture()?; + let bundle = roundtrip(&p)?; + write_new(&out.join("baseline.json"), &serde_json::to_vec_pretty(&p)?)?; + for (path, source) in &bundle.files { + write_new(&out.join("source").join(path), source.as_bytes())?; + } + let names = [ + "crew", + "machine", + "region", + "inner", + "entry", + "exit", + "shared_work", + "node_inner", + "node_entry", + "node_exit", + "entry_route", + "exit_route", + "refresh", + "action_a", + "action_c", + "other_b", + "project", + "state_view", + "state_other", + "sa_view", + "state_a", + "state_b", + "state_c", + "state_leaf", + "node_a", + "node_b", + "node_c", + "node_leaf", + "between", + "external", + "trace", + "sa_function", + "sa_node", + "other_a", + ]; + let ids: std::collections::BTreeMap<_, _> = + names.into_iter().map(|name| (name, id(name))).collect(); + write_new( + &out.join("manifest.json"), + &serde_json::to_vec_pretty(&serde_json::json!({ + "fixture": "state-multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry, + "move_members": ["node_a", "node_b", "node_inner", "node_leaf", "node_entry", "node_exit"], + "translated_routes": ["between", "entry_route", "exit_route"], + "fixed_route": "external", + "window": [1442, 932], + "target_headers_world": {"A": [40, 35], "B": [450, 65]}, + "delta_limits": "finite, nonzero length >=0.01, each axis <=300; no grid quantization", + "receipt": "Pin baseline.json, manifest.json and generation command externally before interaction; baseline identity alone does not authenticate evidence", "fixed_member": "node_c", + "evaluation": "Whole Project invariant comparison; saved A supplies bounded shared delta, not an exact pointer oracle", + "hide": "Remove node_a/node_b only; retain nested Region/leaf/entry/exit, stored routes, all model records and other views", + }))?, + )?; + equal(&p, &load(&out.join("baseline.json"))?)?; + equal(&p, &load(&out.join("source/project.arcadia"))?)?; + println!("PASS generated JSON and source bundle: {}", out.display()); + Ok(()) +} +fn check(mode: &str, baseline: &Path, saved: &Path) -> Result<()> { + let before = load(baseline)?; + let actual = load(saved)?; + validate_candidate(&before)?; + validate_candidate(&actual)?; + if before.id != id("project") { + return Err("Not this fixture's baseline project".into()); + } + let mut expected = before.clone(); + let mut delta = None; + match mode { + "unchanged" => {} + "hide" => { + let view = expected + .diagrams + .get_mut(&id("state_view")) + .ok_or("Missing State view")?; + for alias in ["node_a", "node_b"] { + view.nodes + .remove(&id(alias)) + .ok_or("Baseline selected placement missing")?; + } + // references::remove_objects removes routes only when their semantic + // edge is deleted. Hide deletes placements; both routes MUST survive. + } + "moved" => { + let old = before + .diagrams + .get(&id("state_view")) + .ok_or("Missing baseline State view")?; + let new = actual + .diagrams + .get(&id("state_view")) + .ok_or("Missing saved State view")?; + let a = old + .nodes + .get(&id("node_a")) + .ok_or("Missing baseline A")? + .position; + let moved_a = new + .nodes + .get(&id("node_a")) + .ok_or("Missing saved A")? + .position; + let d = point(moved_a.x - a.x, moved_a.y - a.y); + if !d.x.is_finite() + || !d.y.is_finite() + || d.x.abs().max(d.y.abs()) > 300. + || d.x.hypot(d.y) < 0.01 + { + return Err(format!( + "Move delta must have length >=0.01 and each axis <=300 world units: {d:?}" + ) + .into()); + } + let view = expected.diagrams.get_mut(&id("state_view")).unwrap(); + // Independent explicit fixture enumeration: no group/descendant/ + // routing planner is called to predict the result. + for alias in [ + "node_a", + "node_b", + "node_inner", + "node_leaf", + "node_entry", + "node_exit", + ] { + let node = view + .nodes + .get_mut(&id(alias)) + .ok_or("Missing expected moved placement")?; + node.position.x += d.x; + node.position.y += d.y; + } + for alias in ["between", "entry_route", "exit_route"] { + let route = view + .routes + .iter_mut() + .find(|route| route.edge == id(alias)) + .ok_or("Missing explicitly expected moved route")?; + for pin in &mut route.waypoints { + pin.x += d.x; + pin.y += d.y; + } + } + delta = Some(d); + } + _ => return Err("Mode must be moved, hide, or unchanged".into()), + } + equal(&expected, &actual)?; + roundtrip(&actual)?; + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "result": "PASS", "mode": mode, "baseline": baseline, "saved": saved, + "comparison": "complete Project equality", "inferred_common_delta": delta, + "limits": "Invariant evaluator only; no pointer/action/focus/Undo/source-byte acceptance claim", + }))? + ); + Ok(()) +} +fn main() -> Result<()> { + let args: Vec<_> = std::env::args().skip(1).collect(); + match args.as_slice() { + [command, out] if command == "generate" => generate(Path::new(out)), + [command, mode, baseline, saved] if command == "check" => check(mode, Path::new(baseline), Path::new(saved)), + _ => Err("Usage: state_multiselect_native generate DIRECTORY | check moved|hide|unchanged BASELINE SAVED".into()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + // Produce evidence through real validated commands; the checker itself uses + // explicit fixture enumeration and never calls this movement implementation. + fn moved(before: &Project, delta: Position) -> Project { + let command = diagram_edit::translate_placements( + before, + id("state_view"), + &BTreeSet::from([id("node_a"), id("node_b")]), + delta, + ) + .unwrap(); + let mut session = Session::new(before.clone()); + session.execute(command).unwrap(); + session.project().clone() + } + fn hidden(before: &Project) -> Project { + let mut session = Session::new(before.clone()); + session + .execute(Command::Batch(vec![ + Command::Delete(id("node_a")), + Command::Delete(id("node_b")), + ])) + .unwrap(); + session.project().clone() + } + fn through_saved_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> { + let dir = tempfile::tempdir()?; + let before = dir.path().join("before.json"); + let saved = dir.path().join("saved.json"); + // These assertions distinguish meaningful invariant corruptions from + // malformed projects rejected before the evaluator even sees them. + validate_candidate(baseline).expect("test baseline must be valid"); + validate_candidate(actual).expect("corruption must remain a valid model"); + persistence::save(&before, baseline)?; + persistence::save(&saved, actual)?; + check(mode, &before, &saved) + } + fn mismatch(mode: &str, before: &Project, actual: &Project, case: &str) { + let error = through_saved_json(mode, before, actual) + .expect_err(case) + .to_string(); + assert!( + error.contains("Whole Project mismatch"), + "{case}: unexpected rejection: {error}" + ); + } + fn edge<'a>(p: &'a mut Project, alias: &str) -> &'a mut EdgeRoute { + p.diagrams + .get_mut(&id("state_view")) + .unwrap() + .routes + .iter_mut() + .find(|route| route.edge == id(alias)) + .unwrap() + } + + #[test] + fn generated_json_and_real_source_bundle_are_accepted_unchanged() { + let dir = tempfile::tempdir().unwrap(); + let out = dir.path().join("fixture"); + generate(&out).unwrap(); + let baseline = out.join("baseline.json"); + let saved = out.join("saved.json"); + persistence::save(&saved, &fixture().unwrap()).unwrap(); + check("unchanged", &baseline, &saved).unwrap(); + check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap(); + let bytes = fs::read(&baseline).unwrap(); + assert!( + generate(&out).is_err(), + "generation cannot overwrite evidence" + ); + assert_eq!(fs::read(&baseline).unwrap(), bytes); + let invalid = dir.path().join("invalid.arcadia"); + fs::write(&invalid, "project {").unwrap(); + assert!(load(&invalid).is_err(), "source errors must still reject"); + } + + #[test] + fn real_group_command_outputs_pass_and_are_not_unchanged() { + let before = fixture().unwrap(); + for delta in [ + point(7.25, -2.75), + point(-20.5, 30.125), + point(0.25, 0.), + point(0.01, 10.), + point(0.1, -0.2), + ] { + let actual = moved(&before, delta); + through_saved_json("moved", &before, &actual).unwrap(); + mismatch( + "unchanged", + &before, + &actual, + "real movement cannot count as unchanged", + ); + } + } + + #[test] + fn real_hide_command_outputs_pass_before_and_after_movement() { + let initial = fixture().unwrap(); + for before in [initial.clone(), moved(&initial, point(40., 60.))] { + let actual = hidden(&before); + through_saved_json("hide", &before, &actual).unwrap(); + mismatch( + "unchanged", + &before, + &actual, + "real Hide cannot count as unchanged", + ); + } + } + + #[test] + fn omitted_and_double_moved_child_are_rejected() { + let before = fixture().unwrap(); + let delta = point(40., 60.); + let correct = moved(&before, delta); + for alias in ["node_inner", "node_leaf", "node_entry", "node_exit"] { + let initial = before.diagrams[&id("state_view")].nodes[&id(alias)].position; + for position in [ + initial, + point(initial.x + 2. * delta.x, initial.y + 2. * delta.y), + ] { + let mut bad = correct.clone(); + bad.diagrams + .get_mut(&id("state_view")) + .unwrap() + .nodes + .get_mut(&id(alias)) + .unwrap() + .position = position; + mismatch("moved", &before, &bad, "child did not move exactly once"); + } + } + } + + #[test] + fn internal_pins_left_behind_and_external_pins_moved_are_rejected() { + let mut before = fixture().unwrap(); + let correct = moved(&before, point(40., 60.)); + for alias in ["between", "entry_route", "exit_route"] { + let original_pins = edge(&mut before, alias).waypoints.clone(); + let mut bad = correct.clone(); + edge(&mut bad, alias).waypoints = original_pins; + mismatch("moved", &before, &bad, "internal pins left behind"); + } + let mut bad = correct; + for pin in &mut edge(&mut bad, "external").waypoints { + pin.x += 40.; + pin.y += 60.; + } + mismatch("moved", &before, &bad, "one-ended pins moved"); + } + + #[test] + fn manual_attachments_and_label_data_must_remain_exact() { + let before = fixture().unwrap(); + let correct = moved(&before, point(40., 60.)); + let mutations: [fn(&mut EdgeRoute); 4] = [ + |r| r.source_attachment.as_mut().unwrap().offset = 0.65, + |r| r.target_attachment.as_mut().unwrap().side = PortAnchorSide::Top, + |r| r.label.as_mut().unwrap().offset.x += 40., + |r| r.label.as_mut().unwrap().fraction = 0.75, + ]; + for mutate in mutations { + let mut bad = correct.clone(); + mutate(edge(&mut bad, "between")); + mismatch("moved", &before, &bad, "relative route metadata changed"); + } + } + + #[test] + fn semantic_other_view_fixed_node_and_trace_drift_are_rejected() { + let before = fixture().unwrap(); + let correct = moved(&before, point(40., 60.)); + let mutations: [fn(&mut Project); 9] = [ + |p| p.traces.get_mut(&id("trace")).unwrap().baseline = None, + |p| { + p.elements + .get_mut(&id("shared_work")) + .unwrap() + .description + .push_str(" drift") + }, + |p| { + if let RelationBody::StateAction { phase, .. } = + &mut p.relations.get_mut(&id("action_a")).unwrap().body + { + *phase = StateActionKind::Exit; + } + }, + |p| { + if let RelationBody::StateTransition { guard, .. } = + &mut p.relations.get_mut(&id("refresh")).unwrap().body + { + *guard = "changed_refresh_guard".into(); + } + }, + |p| { + p.elements + .get_mut(&id("state_a")) + .unwrap() + .description + .push_str(" drift") + }, + |p| { + p.diagrams + .get_mut(&id("state_other")) + .unwrap() + .nodes + .get_mut(&id("other_a")) + .unwrap() + .position + .x += 40. + }, + |p| { + p.diagrams + .get_mut(&id("sa_view")) + .unwrap() + .nodes + .get_mut(&id("sa_node")) + .unwrap() + .position + .y += 60. + }, + |p| { + p.diagrams + .get_mut(&id("state_view")) + .unwrap() + .nodes + .get_mut(&id("node_c")) + .unwrap() + .position + .x += 40. + }, + |p| { + p.traces + .get_mut(&id("trace")) + .unwrap() + .description + .push_str(" drift") + }, + ]; + for mutate in mutations { + let mut bad = correct.clone(); + mutate(&mut bad); + mismatch( + "moved", + &before, + &bad, + "record outside permitted movement changed", + ); + } + } + + #[test] + fn zero_and_excessive_moves_cannot_satisfy_move_evidence() { + let before = fixture().unwrap(); + for (delta, reason) in [ + (point(0., 0.), "length >=0.01"), + (point(310., 0.), "each axis <=300"), + ] { + let actual = moved(&before, delta); + let error = through_saved_json("moved", &before, &actual) + .unwrap_err() + .to_string(); + assert!(error.contains(reason), "{delta:?}: {error}"); + } + } + + #[test] + fn hide_must_preserve_child_routes_and_semantic_objects() { + let before = fixture().unwrap(); + let correct = hidden(&before); + let mut missing_child = correct.clone(); + missing_child + .diagrams + .get_mut(&id("state_view")) + .unwrap() + .nodes + .remove(&id("node_leaf")); + mismatch( + "hide", + &before, + &missing_child, + "Hide removed unselected child placement", + ); + let mut missing_routes = correct; + missing_routes + .diagrams + .get_mut(&id("state_view")) + .unwrap() + .routes + .clear(); + mismatch( + "hide", + &before, + &missing_routes, + "Hide removed persistent route records", + ); + let mut semantic_delete = Session::new(before.clone()); + semantic_delete + .execute(Command::Batch(vec![ + Command::Delete(id("state_a")), + Command::Delete(id("state_b")), + ])) + .unwrap(); + mismatch( + "hide", + &before, + semantic_delete.project(), + "Hide became semantic deletion", + ); + } + + #[test] + fn unchanged_rejects_valid_semantic_drift_without_geometry_changes() { + let before = fixture().unwrap(); + let mut actual = before.clone(); + actual + .elements + .get_mut(&id("state_b")) + .unwrap() + .name + .push_str(" edited"); + mismatch( + "unchanged", + &before, + &actual, + "geometry equality is not complete Project equality", + ); + } + + #[test] + fn moved_and_hidden_source_inputs_are_checked_against_complete_expected_projects() { + let before = fixture().unwrap(); + for (mode, actual) in [ + ("moved", moved(&before, point(7.25, -2.75))), + ("hide", hidden(&before)), + ] { + let dir = tempfile::tempdir().unwrap(); + let baseline = dir.path().join("baseline.json"); + persistence::save(&baseline, &before).unwrap(); + let bundle = roundtrip(&actual).unwrap(); + for (path, source) in &bundle.files { + write_new(&dir.path().join("saved").join(path), source.as_bytes()).unwrap(); + } + check( + mode, + &baseline, + &dir.path().join("saved").join(&bundle.entry), + ) + .unwrap(); + let mut corrupt = actual.clone(); + corrupt.elements.get_mut(&id("shared_work")).unwrap().name = + "Unrequested source edit".into(); + let bundle = roundtrip(&corrupt).unwrap(); + for (path, source) in &bundle.files { + write_new(&dir.path().join("corrupt").join(path), source.as_bytes()).unwrap(); + } + let error = check( + mode, + &baseline, + &dir.path().join("corrupt").join(&bundle.entry), + ) + .unwrap_err() + .to_string(); + assert!(error.contains("Whole Project mismatch"), "{error}"); + } + } +} diff --git a/crates/oa-core/src/diagram_edit.rs b/crates/oa-core/src/diagram_edit.rs new file mode 100644 index 00000000..ed0313db --- /dev/null +++ b/crates/oa-core/src/diagram_edit.rs @@ -0,0 +1,91 @@ +//! Pure diagram edits shared by movement previews and committed transactions. +use crate::{Command, Id, MAX_COORDINATE, ModelError, Position, Project, projection}; +use std::collections::{BTreeMap, BTreeSet}; + +/// Translate selected placements and their projected descendants simultaneously. +/// +/// Membership and geometry are read from one snapshot, including folded children. +/// Overlapping roots move each placement once. Route pins follow the combined +/// movement of their actual endpoint owners (and State boundaries). Semantic +/// ownership and other views are untouched. Apply the returned batch through the +/// normal validated command path; its selected identity is not a primary selection. +pub fn translate_placements( + project: &Project, + diagram: Id, + selected: &BTreeSet, + delta: Position, +) -> Result { + let view = project + .diagrams + .get(&diagram) + .ok_or_else(|| ModelError("Movement requires an existing diagram.".into()))?; + if selected.is_empty() { + return Err(ModelError( + "Movement requires at least one placement.".into(), + )); + } + if selected.iter().any(|id| !view.nodes.contains_key(id)) { + return Err(ModelError( + "Every selected placement must belong to the diagram.".into(), + )); + } + if !delta.x.is_finite() || !delta.y.is_finite() { + return Err(ModelError("Movement displacement must be finite.".into())); + } + if delta.x == 0. && delta.y == 0. { + return Ok(Command::Batch(vec![])); + } + let mut children: BTreeMap> = BTreeMap::new(); + for (child, parent) in projection::parents(project, view) { + if let Some(parent) = parent { + children.entry(parent).or_default().push(child); + } + } + let mut moved = BTreeSet::new(); + let mut pending: Vec<_> = selected.iter().copied().collect(); + while let Some(id) = pending.pop() { + if moved.insert(id) { + pending.extend(children.get(&id).into_iter().flatten().copied()); + } + } + let deltas: BTreeMap<_, _> = moved.iter().map(|id| (*id, delta)).collect(); + let routes = projection::translated_routes(project, view, &deltas); + let bounded = |value: f32| value.is_finite() && value.abs() <= MAX_COORDINATE; + let mut commands = Vec::with_capacity(moved.len() + routes.len()); + for id in moved { + let mut node = view.nodes[&id].clone(); + node.position.x += delta.x; + node.position.y += delta.y; + if ![ + node.position.x, + node.position.y, + node.size.width, + node.size.height, + ] + .into_iter() + .all(bounded) + || node.size.width <= 0. + || node.size.height <= 0. + { + return Err(ModelError( + "Movement produces invalid placement bounds.".into(), + )); + } + commands.push(Command::EditNode { + diagram, + original: id, + node, + }); + } + for route in routes { + if route + .waypoints + .iter() + .any(|point| !bounded(point.x) || !bounded(point.y)) + { + return Err(ModelError("Movement produces invalid route bounds.".into())); + } + commands.push(Command::SetEdgeRoute { diagram, route }); + } + Ok(Command::Batch(commands)) +} diff --git a/crates/oa-core/src/lib.rs b/crates/oa-core/src/lib.rs index 940deda6..a603ced0 100644 --- a/crates/oa-core/src/lib.rs +++ b/crates/oa-core/src/lib.rs @@ -1,6 +1,7 @@ //! Framework-independent OA/SA model, transactional commands and editing services. pub mod analysis_numeric; pub mod data; +pub mod diagram_edit; pub mod language; pub mod model; pub mod operations; diff --git a/crates/oa-core/tests/group_movement.rs b/crates/oa-core/tests/group_movement.rs new file mode 100644 index 00000000..9afbec87 --- /dev/null +++ b/crates/oa-core/tests/group_movement.rs @@ -0,0 +1,427 @@ +use oa_core::{diagram_edit::translate_placements, source_project::*, *}; +use std::collections::BTreeSet; + +fn point(x: f32, y: f32) -> Position { + Position { x, y } +} +fn route(edge: Id, x: f32, y: f32) -> EdgeRoute { + EdgeRoute { + edge, + source_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Bottom, + offset: 0.2, + }), + target_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Top, + offset: 0.8, + }), + waypoints: vec![point(x, y)], + label: Some(EdgeLabel { + fraction: 0.4, + offset: point(3., -9.), + }), + } +} +struct Fixture { + project: Project, + diagram: Id, + a: Id, + b: Id, + child: Id, + foreign: Id, +} +fn fixture() -> Fixture { + let mut p = Project::empty(); + let a = p.add_element(Kind::Activity, Layer::Oa, "A", None).unwrap(); + let b = p.add_element(Kind::Activity, Layer::Oa, "B", None).unwrap(); + let c = p.add_element(Kind::Activity, Layer::Oa, "C", None).unwrap(); + let child = p + .add_element(Kind::Activity, Layer::Oa, "Child", Some(a)) + .unwrap(); + let internal = p.relate(RelationKind::Interaction, "Internal", a, b); + let external = p.relate(RelationKind::Interaction, "External", b, c); + let d = p.ensure_diagram(Layer::Oa); + let na = p.place(d, a, point(10., 20.)); + let nb = p.place(d, b, point(400., 20.)); + let nc = p.place(d, child, point(30., 80.)); + p.place(d, c, point(800., 20.)); + p.diagrams.get_mut(&d).unwrap().routes = + vec![route(internal, 320., 150.), route(external, 720., 170.)]; + p.diagrams + .get_mut(&d) + .unwrap() + .nodes + .get_mut(&na) + .unwrap() + .collapsed = true; + let other = Id::new_v4(); + let mut other_view = p.diagrams[&d].clone(); + other_view.id = other; + other_view.alias = "other_view".into(); + other_view.nodes.clear(); + other_view.routes.clear(); + p.diagrams.insert(other, other_view); + let foreign = p.place(other, a, point(-40., -50.)); + validate_candidate(&p).unwrap(); + Fixture { + project: p, + diagram: d, + a: na, + b: nb, + child: nc, + foreign, + } +} + +#[test] +fn simultaneous_siblings_translate_internal_pins_once_and_preserve_complete_project() { + let f = fixture(); + let before = f.project.clone(); + let command = translate_placements( + &before, + f.diagram, + &BTreeSet::from([f.a, f.b]), + point(40., 60.), + ) + .unwrap(); + assert_eq!( + before, f.project, + "command construction must not mutate input" + ); + // Independently enumerated expected edits, without the projection helpers. + let mut expected = before.clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.get_mut(&f.a).unwrap().position = point(50., 80.); + view.nodes.get_mut(&f.b).unwrap().position = point(440., 80.); + view.nodes.get_mut(&f.child).unwrap().position = point(70., 140.); + view.routes[0].waypoints = vec![point(360., 210.)]; + let mut session = Session::new(before.clone()); + session.execute(command.clone()).unwrap(); + assert_eq!(session.project(), &expected); + session.undo(); + assert_eq!(session.project(), &before); + assert!( + !session.can_undo(), + "one movement must be one history entry" + ); + session.redo(); + assert_eq!(session.project(), &expected); + assert!(!session.can_redo()); + + // Independent one-node moves cannot see the shared endpoint displacement. + let mut sequential = Session::new(before.clone()); + for (node, position) in [(f.a, point(50., 80.)), (f.b, point(440., 80.))] { + sequential + .execute(Command::MoveNode { + diagram: f.diagram, + node, + position, + with_descendants: true, + }) + .unwrap(); + } + assert_eq!( + sequential.project().diagrams[&f.diagram].routes[0], + before.diagrams[&f.diagram].routes[0] + ); + assert_ne!(sequential.project(), &expected); + + let json = serde_json::to_string(&expected).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), expected); + let mut bundle = format_bundle(&before); + bundle + .files + .get_mut(&bundle.entry.clone()) + .unwrap() + .insert_str(0, "// preserve movement rationale\n"); + let original = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); + let prepared = prepare_command(&before, &command).unwrap(); + let updated = update_bundle_with_identity_map( + &original, + &prepared.project, + &prepared.identity_map, + ProjectLimits::default(), + ) + .unwrap(); + assert_eq!(updated.decoded.project, expected); + assert!(updated.bundle.files[&bundle.entry].starts_with("// preserve movement rationale\n")); + let reread = compile_bundle(&updated.bundle, ProjectLimits::default()).unwrap(); + assert_eq!(reread.decoded.project, expected); + assert_eq!(reread.bundle, updated.bundle); +} + +#[test] +fn selecting_folded_ancestor_and_hidden_descendant_moves_each_once() { + let f = fixture(); + assert!( + !projection::project_view(&f.project, f.diagram) + .nodes + .iter() + .any(|n| n.id == f.child) + ); + let ancestor = translate_placements( + &f.project, + f.diagram, + &BTreeSet::from([f.a]), + point(-15., 25.), + ) + .unwrap(); + let overlap = translate_placements( + &f.project, + f.diagram, + &BTreeSet::from([f.a, f.child]), + point(-15., 25.), + ) + .unwrap(); + assert_eq!(overlap, ancestor); + let after = prepare_command(&f.project, &overlap).unwrap().project; + assert_eq!( + after.diagrams[&f.diagram].nodes[&f.child].position, + point(15., 105.) + ); + assert_eq!( + after.diagrams[&f.diagram].routes, + f.project.diagrams[&f.diagram].routes + ); +} + +#[test] +fn invalid_movements_refuse_atomically_and_zero_has_no_history() { + let f = fixture(); + let selected = BTreeSet::from([f.a, f.b]); + for (diagram, ids, delta) in [ + (Id::new_v4(), selected.clone(), point(1., 1.)), + (f.diagram, BTreeSet::new(), point(1., 1.)), + ( + f.diagram, + BTreeSet::from([f.a, Id::new_v4()]), + point(1., 1.), + ), + (f.diagram, BTreeSet::from([f.a, f.foreign]), point(0., 0.)), + (f.diagram, selected.clone(), point(f32::NAN, 1.)), + (f.diagram, selected.clone(), point(1., f32::INFINITY)), + (f.diagram, selected.clone(), point(MAX_COORDINATE, 0.)), + (f.diagram, selected.clone(), point(f32::MAX, f32::MAX)), + ] { + let before = f.project.clone(); + assert!(translate_placements(&f.project, diagram, &ids, delta).is_err()); + assert_eq!(f.project, before); + } + let mut bad_route = f.project.clone(); + bad_route.diagrams.get_mut(&f.diagram).unwrap().routes[0].waypoints = + vec![point(MAX_COORDINATE, 0.)]; + validate_candidate(&bad_route).unwrap(); + let snapshot = bad_route.clone(); + assert!(translate_placements(&bad_route, f.diagram, &selected, point(1., 0.)).is_err()); + assert_eq!(bad_route, snapshot); + let zero = translate_placements(&f.project, f.diagram, &selected, point(0., -0.)).unwrap(); + assert_eq!(zero, Command::Batch(vec![])); + let mut session = Session::new(f.project.clone()); + session.execute(zero).unwrap(); + assert_eq!(session.project(), &f.project); + assert!(!session.can_undo()); + assert_eq!(session.revision(), 0); +} + +#[test] +fn typed_component_port_endpoints_follow_their_owner_placements() { + let mut p = Project::empty(); + let a = p + .add_element(Kind::System, Layer::Sa, "System", None) + .unwrap(); + let b = p + .add_element(Kind::SystemActor, Layer::Sa, "Actor", None) + .unwrap(); + let output = Id::new_v4(); + let input = Id::new_v4(); + for (id, owner, name, orientation) in [ + (output, a, "Output", ComponentPortOrientation::Out), + (input, b, "Input", ComponentPortOrientation::In), + ] { + let port = make_port( + &p, + id, + owner, + name, + PortContract::Component { + kind: ComponentPortKind::Flow, + orientation, + }, + ) + .unwrap(); + p.elements.insert(id, port); + } + let edge = p.relate(RelationKind::BoundaryExchange, "Boundary", output, input); + let d = p.ensure_diagram(Layer::Sa); + let na = p.place(d, a, point(0., 0.)); + let nb = p.place(d, b, point(400., 0.)); + for (node, port, side, offset) in [ + (na, output, PortAnchorSide::Bottom, 0.3), + (nb, input, PortAnchorSide::Top, 0.7), + ] { + p.diagrams + .get_mut(&d) + .unwrap() + .nodes + .get_mut(&node) + .unwrap() + .port_anchors + .push(PortAnchor { port, side, offset }); + } + p.diagrams + .get_mut(&d) + .unwrap() + .routes + .push(route(edge, 300., 100.)); + validate_candidate(&p).unwrap(); + let command = translate_placements(&p, d, &BTreeSet::from([na, nb]), point(10., -20.)).unwrap(); + let after = prepare_command(&p, &command).unwrap().project; + let mut expected = p.clone(); + let view = expected.diagrams.get_mut(&d).unwrap(); + view.nodes.get_mut(&na).unwrap().position = point(10., -20.); + view.nodes.get_mut(&nb).unwrap().position = point(410., -20.); + view.routes[0].waypoints = vec![point(310., 80.)]; + assert_eq!(after, expected); +} + +#[test] +fn allocation_movement_follows_visual_performers_and_same_performer_work_groups() { + let mut p = Project::empty(); + let performer_a = p + .add_element(Kind::Entity, Layer::Oa, "Performer A", None) + .unwrap(); + let performer_b = p + .add_element(Kind::Entity, Layer::Oa, "Performer B", None) + .unwrap(); + let mixed = p + .add_element(Kind::Activity, Layer::Oa, "Mixed work", None) + .unwrap(); + let branch = p + .add_element(Kind::Activity, Layer::Oa, "A work", Some(mixed)) + .unwrap(); + let leaf_a = p + .add_element(Kind::Activity, Layer::Oa, "A leaf", Some(branch)) + .unwrap(); + let leaf_b = p + .add_element(Kind::Activity, Layer::Oa, "B leaf", Some(mixed)) + .unwrap(); + p.relate(RelationKind::Allocation, "A performs", performer_a, leaf_a); + p.relate(RelationKind::Allocation, "B performs", performer_b, leaf_b); + let d = p.ensure_diagram(Layer::Oa); + p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Allocation; + let na = p.place(d, performer_a, point(0., 0.)); + p.place(d, performer_b, point(600., 0.)); + let nm = p.place(d, mixed, point(1000., 0.)); + let branch_node = p.place(d, branch, point(20., 80.)); + let leaf_node = p.place(d, leaf_a, point(40., 160.)); + p.place(d, leaf_b, point(620., 80.)); + validate_candidate(&p).unwrap(); + + // Mixed work retains semantic ownership of both branches, but its leaves + // have different performers. A work is visually nested under Performer A, + // with its own same-performer leaf nested inside it. + let command = + translate_placements(&p, d, &BTreeSet::from([na, branch_node]), point(15., -25.)).unwrap(); + let after = prepare_command(&p, &command).unwrap().project; + let mut expected = p.clone(); + let view = expected.diagrams.get_mut(&d).unwrap(); + view.nodes.get_mut(&na).unwrap().position = point(15., -25.); + view.nodes.get_mut(&branch_node).unwrap().position = point(35., 55.); + view.nodes.get_mut(&leaf_node).unwrap().position = point(55., 135.); + assert_eq!(after, expected); + + // Moving the semantic parent does not move children displayed elsewhere. + let command = translate_placements(&p, d, &BTreeSet::from([nm]), point(15., -25.)).unwrap(); + let after = prepare_command(&p, &command).unwrap().project; + let mut expected = p.clone(); + expected + .diagrams + .get_mut(&d) + .unwrap() + .nodes + .get_mut(&nm) + .unwrap() + .position = point(1015., -25.); + assert_eq!(after, expected); +} + +#[test] +fn traceability_movement_is_flat_even_for_a_semantic_parent() { + let mut f = fixture(); + f.project.diagrams.get_mut(&f.diagram).unwrap().projection = ProjectionKind::Traceability; + validate_candidate(&f.project).unwrap(); + let command = translate_placements( + &f.project, + f.diagram, + &BTreeSet::from([f.a]), + point(40., 60.), + ) + .unwrap(); + let after = prepare_command(&f.project, &command).unwrap().project; + let mut expected = f.project.clone(); + expected + .diagrams + .get_mut(&f.diagram) + .unwrap() + .nodes + .get_mut(&f.a) + .unwrap() + .position = point(50., 80.); + assert_eq!( + after, expected, + "flat view movement must preserve semantic descendants, routes, other views and all semantics" + ); +} + +#[test] +fn state_entry_pins_follow_the_boundary_only_when_it_also_moves() { + let mut p = Project::empty(); + let machine = p + .add_element(Kind::StateMachine, Layer::Oa, "Machine", None) + .unwrap(); + let region = p + .add_element(Kind::Region, Layer::Oa, "Region", Some(machine)) + .unwrap(); + let composite = p + .add_element(Kind::State, Layer::Oa, "Composite", Some(region)) + .unwrap(); + let inner = p + .add_element(Kind::Region, Layer::Oa, "Inner", Some(composite)) + .unwrap(); + let entry = p + .add_element(Kind::EntryPoint, Layer::Oa, "Entry", Some(inner)) + .unwrap(); + let leaf = p + .add_element(Kind::State, Layer::Oa, "Leaf", Some(inner)) + .unwrap(); + let edge = p.relate(RelationKind::StateTransition, "Enter", entry, leaf); + if let RelationBody::StateTransition { + transition_kind, .. + } = &mut p.relations.get_mut(&edge).unwrap().body + { + *transition_kind = TransitionKind::Local; + } + let d = p.ensure_diagram(Layer::Oa); + p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::StateMachine { machine }; + let boundary = p.place(d, composite, point(0., 0.)); + let ne = p.place(d, entry, point(20., 80.)); + let nl = p.place(d, leaf, point(100., 80.)); + p.diagrams + .get_mut(&d) + .unwrap() + .routes + .push(route(edge, 90., 100.)); + validate_candidate(&p).unwrap(); + let endpoints = + translate_placements(&p, d, &BTreeSet::from([ne, nl]), point(10., 20.)).unwrap(); + let after = prepare_command(&p, &endpoints).unwrap().project; + assert_eq!(after.diagrams[&d].routes, p.diagrams[&d].routes); + let with_boundary = + translate_placements(&p, d, &BTreeSet::from([boundary, ne, nl]), point(10., 20.)).unwrap(); + let after = prepare_command(&p, &with_boundary).unwrap().project; + assert_eq!( + after.diagrams[&d].routes[0].waypoints, + vec![point(100., 120.)] + ); + assert_eq!(after.diagrams[&d].nodes[&ne].position, point(30., 100.)); +} diff --git a/crates/oa-desktop/src/allocation_input.rs b/crates/oa-desktop/src/allocation_input.rs index cda510c0..21442bd5 100644 --- a/crates/oa-desktop/src/allocation_input.rs +++ b/crates/oa-desktop/src/allocation_input.rs @@ -60,6 +60,7 @@ impl Root<'_> { return false; } let canvas = DiagramCanvas { + selection: self.app.diagram_selection.as_ref(), allocation: Some(self.app.allocation_capability()), project: self.app.project(), view_index: Some(self.app.document.view_index()), diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index fa73b4e2..5122506a 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -15,6 +15,9 @@ mod diagram_reveal; mod diagram_selection_tests; mod diagram_usages; mod hosted; +pub(crate) mod multiselect; +#[cfg(test)] +mod multiselect_tests; mod reference_authoring; #[cfg(test)] mod review_navigation_tests; @@ -26,6 +29,10 @@ mod scenario_spacing; mod source_indentation_tests; pub(crate) mod source_rename; mod source_symbols; +#[cfg(test)] +mod state_multiselect_focus_tests; +#[cfg(test)] +mod state_multiselect_tests; pub(crate) mod text_workspace; #[cfg(test)] mod transition_context_tests; @@ -371,6 +378,7 @@ pub enum Message { Select(Id), EditProperties(Id), SelectNode(Id), + DiagramSelection(multiselect::Event), SelectProject, Search(String), StartTransition, @@ -515,6 +523,7 @@ pub struct App { pub layer: Layer, pub selected: Id, pub selected_node: Option, + pub diagram_selection: Option, pub draft: Option, pub active_diagram: Option, pub search: String, @@ -636,6 +645,7 @@ impl App { layer, selected, selected_node: None, + diagram_selection: None, draft, active_diagram, search: String::new(), @@ -1337,6 +1347,38 @@ impl App { ]) } pub fn update(&mut self, message: Message) -> Task { + let selection_boundary = self.multiselect_boundary(); + // Reject delayed gestures before they can retire newer input ownership. + if let Message::DiagramSelection(event) + | Message::StateDiagram(crate::behavior::state_edit::Event::Selection(event)) = &message + && !self.multiselect_envelope(event) + { + return Task::none(); + } + if let Message::StateDiagram(event) = &message + && let Some(captured) = event.context() + && self.state_context().is_none_or(|current| { + crate::behavior::state_edit::validate_context(self.project(), captured, current) + .is_err() + }) + { + return Task::none(); + } + if matches!( + &message, + Message::StateDiagram( + crate::behavior::state_edit::Event::Select(..) + | crate::behavior::state_edit::Event::Move { .. } + | crate::behavior::state_edit::Event::Remove(_) + ) + ) && self + .multiselect_snapshot() + .is_some_and(|s| s.targets().len() > 1) + { + // A queued singleton cannot retire the current group's navigation + // or allocation ownership before its edit is refused. + return Task::none(); + } // Captured Text commands validate their origin before they can // invalidate that same origin or pass into existing command handlers. if let Message::TextWorkspace(event) = message { @@ -1439,7 +1481,11 @@ impl App { self.reconcile_pending_diagram_reveal(&message); self.allocation_before(&message); if let Message::WorkDrag(event) = message { - return self.allocation_event(event); + let task = self.allocation_event(event); + if self.multiselect_boundary() != selection_boundary { + self.reconcile_multiselect(); + } + return task; } let allocation_before = self.allocation_boundary(); if let Message::BehaviorControl(intent) = message { @@ -1480,6 +1526,11 @@ impl App { self.camera_epoch = self.camera_epoch.wrapping_add(1); self.camera_boundary_epoch = self.canvas_epoch; } + if self.multiselect_boundary() != selection_boundary + || (self.diagram_selection.is_none() && self.multiselect_context().is_some()) + { + self.reconcile_multiselect(); + } task } fn behavior_control(&mut self, intent: behavior::canvas_workspace::Intent) -> Task { @@ -1847,6 +1898,7 @@ impl App { message, Message::Page(_) | Message::Layer(_) | Message::Diagram(_) ) { + self.diagram_selection = None; self.canvas_epoch = self.canvas_epoch.wrapping_add(1); self.selected_bend = None; } @@ -2563,8 +2615,17 @@ impl App { "Apply or revert pending edits before using modeling actions.".into(); return Task::none(); } + if matches!(*action, Message::DiagramSelection(_)) { + return self.update(*action); + } self.select(menu.request.object); - self.selected_node = menu.request.placement; + if self.page == Page::Behavior && menu.request.canvas.is_some() { + self.behavior.diagram.placement = menu.request.placement; + self.selected_node = None; + } else { + self.selected_node = menu.request.placement; + } + self.reconcile_multiselect(); self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i)); return self.update(*action); } @@ -2722,6 +2783,7 @@ impl App { self.status = "Property draft reverted".into(); } Message::Select(id) => self.select(id), + Message::DiagramSelection(event) => self.multiselect_event(event), Message::EditProperties(id) => { self.select(id); self.inspector_tab = crate::navigation::InspectorTab::Properties; @@ -3395,7 +3457,21 @@ impl App { } } } - Message::DiagramRemove(intent) => self.remove_from_diagram(intent), + Message::DiagramRemove(intent) => { + if self + .multiselect_snapshot() + .is_some_and(|s| s.targets().len() > 1) + { + // A pre-redraw singleton Delete cannot remove just one + // member of a selection changed earlier in the same batch. + if Some(intent.context) == self.route_context() { + self.status = + "Selection changed. Use Hide selected or press Delete again.".into(); + } + } else { + self.remove_from_diagram(intent); + } + } Message::DiagramTraverse(back) => { return if back { iced::widget::operation::focus_previous() @@ -3404,6 +3480,13 @@ impl App { }; } Message::Delete => { + if self + .multiselect_snapshot() + .is_some_and(|s| s.targets().len() > 1) + { + self.status = "Group model-deletion review is not available yet. Select one element or use Hide selected.".into(); + return Task::none(); + } if self.selected != self.project().id && self.apply() { self.begin_model_delete(None); } @@ -3556,6 +3639,13 @@ impl App { } } Message::HideSelected => { + if let Some(snapshot) = self + .multiselect_snapshot() + .filter(|s| s.targets().len() > 1) + { + self.multiselect_event(multiselect::Event::Hide(snapshot)); + return Task::none(); + } if self.apply() && let Some(node) = self.selected_node { @@ -3585,6 +3675,13 @@ impl App { } Message::Route(event) => self.edit_route(event), Message::MoveNode(node, position) => { + if self + .multiselect_snapshot() + .is_some_and(|s| s.targets().len() > 1) + { + self.status = "Selection changed. Start the group drag again.".into(); + return Task::none(); + } let original = self.selected; if self.apply() && let Some(diagram) = self.active_diagram diff --git a/crates/oa-desktop/src/app/allocation_drag_tests.rs b/crates/oa-desktop/src/app/allocation_drag_tests.rs index ee5794ea..af02e7f5 100644 --- a/crates/oa-desktop/src/app/allocation_drag_tests.rs +++ b/crates/oa-desktop/src/app/allocation_drag_tests.rs @@ -59,6 +59,7 @@ fn fixture(layer: Layer) -> Fixture { } fn canvas(app: &App) -> DiagramCanvas<'_> { DiagramCanvas { + selection: None, allocation: Some(app.allocation_capability()), project: app.project(), view_index: Some(app.document.view_index()), diff --git a/crates/oa-desktop/src/app/architecture_authoring/tests.rs b/crates/oa-desktop/src/app/architecture_authoring/tests.rs index 18ee63c3..2716b2ae 100644 --- a/crates/oa-desktop/src/app/architecture_authoring/tests.rs +++ b/crates/oa-desktop/src/app/architecture_authoring/tests.rs @@ -66,6 +66,7 @@ fn architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo() { let camera = (app.zoom, app.pan); let bounds = iced::Rectangle::new(iced::Point::ORIGIN, iced::Size::new(600., 380.)); let canvas = crate::diagram::DiagramCanvas { + selection: None, project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), diff --git a/crates/oa-desktop/src/app/architecture_authoring_tests.rs b/crates/oa-desktop/src/app/architecture_authoring_tests.rs index 4c0aabb0..44843766 100644 --- a/crates/oa-desktop/src/app/architecture_authoring_tests.rs +++ b/crates/oa-desktop/src/app/architecture_authoring_tests.rs @@ -221,6 +221,7 @@ fn architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan() { y: (local.y - app.pan.y) / app.zoom, }; let canvas = crate::diagram::DiagramCanvas { + selection: None, project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), @@ -298,6 +299,7 @@ fn architecture_f2_rename_preserves_identity_placement_and_atomic_history() { let diagrams = app.project().diagrams.clone(); let camera = (app.zoom, app.pan); let canvas = crate::diagram::DiagramCanvas { + selection: None, project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), diff --git a/crates/oa-desktop/src/app/diagram_focus_tests.rs b/crates/oa-desktop/src/app/diagram_focus_tests.rs index d029f783..345cfa83 100644 --- a/crates/oa-desktop/src/app/diagram_focus_tests.rs +++ b/crates/oa-desktop/src/app/diagram_focus_tests.rs @@ -154,3 +154,440 @@ async fn mounted_f2_requires_canvas_focus_and_yields_to_clean_inspector() { } } } +async fn modifier_focus_fixture() -> (App, Mounted, Id, Id, Id) { + let mut project = Project::empty(); + let a = project + .add_element(Kind::Entity, Layer::Oa, "First team", None) + .unwrap(); + let b = project + .add_element(Kind::Entity, Layer::Oa, "Second team", None) + .unwrap(); + let diagram = project.ensure_diagram(Layer::Oa); + let first = project.place(diagram, a, Position { x: 80., y: 80. }); + let second = project.place(diagram, b, Position { x: 500., y: 80. }); + let mut app = App::with_project(project); + app.page = Page::Diagram; + app.layer = Layer::Oa; + app.active_diagram = Some(diagram); + app.window_size = Size::new(2000., 1400.); + let _ = app.update(Message::Select(a)); + let mut mounted = Mounted::new(&mut app).await; + app.zoom = 1.; + app.pan = Vector::ZERO; + app.fit_to_view = false; + mounted.settle(&mut app).await; + (app, mounted, diagram, first, second) +} + +fn modifier_node_point(mounted: &mut Mounted, app: &App, diagram: Id, placement: Id) -> Point { + let bounds = mounted + .geometry(app) + .bounds(crate::allocation_input::CANVAS_ID); + let node = projection::project_view(app.project(), diagram) + .nodes + .into_iter() + .find(|n| n.id == placement) + .unwrap(); + Point::new( + bounds.x + node.position.x + 40., + bounds.y + node.position.y + 35., + ) +} + +fn modifier_canvas_focused(mounted: &mut Mounted, app: &App) -> bool { + mounted + .geometry(app) + .focused + .iter() + .find(|(id, _, _)| { + id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner")) + }) + .expect("production focus::Owner is mounted") + .2 +} + +fn modifier_event(shift: bool) -> iced::Event { + iced::Event::Keyboard(iced::keyboard::Event::ModifiersChanged(if shift { + iced::keyboard::Modifiers::SHIFT + } else { + iced::keyboard::Modifiers::default() + })) +} + +#[tokio::test] +async fn mounted_shift_before_canvas_focus_toggles_exact_node_without_granting_keyboard_focus() { + use crate::diagram_usage::Target; + let (mut app, mut mounted, diagram, first, second) = modifier_focus_fixture().await; + let before = app.document.snapshot(); + let field = mounted + .geometry(&app) + .text_inputs + .into_iter() + .filter(|bounds| bounds.x > app.window_size.width - 320.) + .min_by(|a, b| a.y.total_cmp(&b.y)) + .expect("clean Inspector name input"); + mounted.operate(&app, &mut FocusField(field)); + assert!(!modifier_canvas_focused(&mut mounted, &app)); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first)] + ); + mounted + .batch( + &mut app, + &[modifier_event(true)], + mouse::Cursor::Unavailable, + ) + .await; + assert!( + !modifier_canvas_focused(&mut mounted, &app), + "modifier tracking must not acquire focus" + ); + let at = modifier_node_point(&mut mounted, &app, diagram, second); + let messages = mounted.click(&mut app, at).await; + assert!(messages.iter().any(|m| matches!(m, + Message::DiagramSelection(multiselect::Event::Choose(_, Target::Placement(id), multiselect::Choice::Toggle)) if *id == second + )), "Shift already held before the first canvas press must reach its exact-target adapter"); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first), Target::Placement(second)] + ); + assert_eq!(app.selected_node, Some(second)); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + assert!(!app.draft_dirty()); +} + +#[tokio::test] +async fn mounted_unfocused_typing_and_delete_remain_denied_after_passive_modifiers() { + use crate::diagram_usage::Target; + let (mut app, mut mounted, diagram, first, second) = modifier_focus_fixture().await; + mounted + .batch( + &mut app, + &[modifier_event(true)], + mouse::Cursor::Unavailable, + ) + .await; + let at = modifier_node_point(&mut mounted, &app, diagram, second); + mounted.click(&mut app, at).await; + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first), Target::Placement(second)] + ); + // Revoke every field's focus through the real widget operation. No widget + // has this off-window rectangle, so neither Inspector nor canvas owns keys. + mounted.operate( + &app, + &mut FocusField(Rectangle { + x: -1000., + y: -1000., + width: 1., + height: 1., + }), + ); + assert!(!modifier_canvas_focused(&mut mounted, &app)); + let before = app.document.snapshot(); + let selection = app.multiselect_snapshot().unwrap(); + let messages = mounted + .batch( + &mut app, + &[ + modifier_event(false), + crate::behavior::canvas_workspace::tests::character("x"), + key(Named::Delete), + key(Named::F2), + ], + mouse::Cursor::Unavailable, + ) + .await; + assert!(!messages.iter().any(|m| matches!( + m, + Message::DiagramSelection(_) + | Message::DiagramRemove(_) + | Message::HideSelected + | Message::Delete + ))); + assert!(!rename_emitted(&messages)); + assert!(!modifier_canvas_focused(&mut mounted, &app)); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + selection.targets() + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + assert!(!app.draft_dirty()); + assert!(!app.delete_pending); +} + +#[tokio::test] +async fn mounted_suspended_modifiers_are_passive_and_survive_until_resumed_click() { + use crate::diagram_usage::Target; + let (mut app, mut mounted, diagram, first, second) = modifier_focus_fixture().await; + let before = app.document.snapshot(); + let at = modifier_node_point(&mut mounted, &app, diagram, second); + app.busy = true; + mounted.settle(&mut app).await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + mounted + .batch( + &mut app, + &[modifier_event(true), key(Named::Delete)], + mouse::Cursor::Unavailable, + ) + .await; + mounted.click(&mut app, at).await; + assert!( + !modifier_canvas_focused(&mut mounted, &app), + "passive I/O cannot grant canvas focus" + ); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first)] + ); + assert_eq!(app.document.bundle(), &before.bundle); + app.busy = false; + mounted.settle(&mut app).await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + mounted.click(&mut app, at).await; + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first), Target::Placement(second)] + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); +} + +#[tokio::test] +async fn mounted_shift_held_before_suspension_survives_without_another_modifier_event() { + use crate::diagram_usage::Target; + let (mut app, mut mounted, diagram, first, second) = modifier_focus_fixture().await; + let before = app.document.snapshot(); + let field = mounted + .geometry(&app) + .text_inputs + .into_iter() + .filter(|bounds| bounds.x > app.window_size.width - 320.) + .min_by(|a, b| a.y.total_cmp(&b.y)) + .expect("clean Inspector name input"); + mounted.operate(&app, &mut FocusField(field)); + mounted + .batch( + &mut app, + &[modifier_event(true)], + mouse::Cursor::Unavailable, + ) + .await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + app.busy = true; + mounted.settle(&mut app).await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + app.busy = false; + mounted.settle(&mut app).await; + assert!( + !modifier_canvas_focused(&mut mounted, &app), + "restoring passive modifiers must not grant keyboard focus" + ); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first)] + ); + // No modifier transition follows the subtree replacement during I/O. + let at = modifier_node_point(&mut mounted, &app, diagram, second); + let messages = mounted.click(&mut app, at).await; + assert!(messages.iter().any(|m| matches!(m, + Message::DiagramSelection(multiselect::Event::Choose(_, Target::Placement(id), multiselect::Choice::Toggle)) if *id == second + ))); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first), Target::Placement(second)] + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + assert!(!app.draft_dirty()); +} + +#[tokio::test] +async fn mounted_shift_release_or_window_unfocus_during_suspension_does_not_restore_stale_shift() { + use crate::diagram_usage::Target; + for release in [ + modifier_event(false), + iced::Event::Window(iced::window::Event::Unfocused), + ] { + let (mut app, mut mounted, diagram, first, second) = modifier_focus_fixture().await; + let before = app.document.snapshot(); + mounted + .batch( + &mut app, + &[modifier_event(true)], + mouse::Cursor::Unavailable, + ) + .await; + app.busy = true; + mounted.settle(&mut app).await; + mounted + .batch(&mut app, &[release], mouse::Cursor::Unavailable) + .await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + app.busy = false; + mounted.settle(&mut app).await; + // A further I/O cycle must restore the updated owner state too. + app.busy = true; + mounted.settle(&mut app).await; + app.busy = false; + mounted.settle(&mut app).await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(first)] + ); + let at = modifier_node_point(&mut mounted, &app, diagram, second); + let messages = mounted.click(&mut app, at).await; + assert!(!messages.iter().any(|m| matches!( + m, + Message::DiagramSelection(multiselect::Event::Choose( + _, + _, + multiselect::Choice::Toggle + )) + ))); + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(second)] + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + assert!(!app.draft_dirty()); + } +} + +#[tokio::test] +async fn mounted_window_unfocus_resets_held_shift_before_next_canvas_press() { + use crate::diagram_usage::Target; + let (mut app, mut mounted, diagram, first, second) = modifier_focus_fixture().await; + let at_first = modifier_node_point(&mut mounted, &app, diagram, first); + mounted.click(&mut app, at_first).await; + let before = app.document.snapshot(); + mounted + .batch( + &mut app, + &[ + modifier_event(true), + iced::Event::Window(iced::window::Event::Unfocused), + ], + mouse::Cursor::Unavailable, + ) + .await; + assert!(!modifier_canvas_focused(&mut mounted, &app)); + // No ModifiersChanged(empty) follows: a lost window must itself reset it. + let at_second = modifier_node_point(&mut mounted, &app, diagram, second); + mounted.click(&mut app, at_second).await; + assert_eq!( + app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(second)] + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); +} + +#[tokio::test] +async fn mounted_selection_strip_keeps_canvas_and_fitted_camera_fixed_for_empty_single_group_and_mixed() + { + use crate::diagram_usage::Target; + let mut project = Project::empty(); + let a = project + .add_element(Kind::Activity, Layer::Oa, "First activity", None) + .unwrap(); + let b = project + .add_element( + Kind::Activity, + Layer::Oa, + &format!( + "Second activity {}", + "with a very long primary name ".repeat(20) + ), + None, + ) + .unwrap(); + let edge = project.relate(RelationKind::Interaction, "Activity exchange", a, b); + let diagram = project.ensure_diagram(Layer::Oa); + project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview; + let first = project.place(diagram, a, Position { x: 80., y: 80. }); + let second = project.place(diagram, b, Position { x: 500., y: 80. }); + let mut app = App::with_project(project); + app.page = Page::Diagram; + app.layer = Layer::Oa; + app.active_diagram = Some(diagram); + app.window_size = Size::new(1442., 932.); + let _ = app.update(Message::Select(diagram)); + app.fit_to_view = true; + let mut mounted = Mounted::new(&mut app).await; + let baseline = app.document.snapshot(); + let bounds = mounted + .geometry(&app) + .bounds(crate::allocation_input::CANVAS_ID); + let camera = (app.zoom, app.pan, app.fit_to_view, app.canvas_size); + assert!(camera.2, "exercise the native resize-triggered refit path"); + assert!(app.multiselect_snapshot().unwrap().targets().is_empty()); + assert!(bounds.width > 500. && bounds.height > 300.); + for (target, expected) in [ + ( + Some(Target::Placement(first)), + vec![Target::Placement(first)], + ), + ( + Some(Target::Placement(second)), + vec![Target::Placement(first), Target::Placement(second)], + ), + ( + Some(Target::Edge(edge)), + vec![ + Target::Placement(first), + Target::Placement(second), + Target::Edge(edge), + ], + ), + ( + Some(Target::Edge(edge)), + vec![Target::Placement(first), Target::Placement(second)], + ), + ( + Some(Target::Placement(second)), + vec![Target::Placement(first)], + ), + (None, vec![]), + ] { + let context = app.multiselect_context().unwrap(); + let event = match target { + Some(target) => { + multiselect::Event::Choose(context, target, multiselect::Choice::Toggle) + } + None => multiselect::Event::Clear(context), + }; + let task = app.update(Message::DiagramSelection(event)); + mounted.perform(&mut app, task).await; + mounted.settle(&mut app).await; + assert_eq!(app.multiselect_snapshot().unwrap().targets(), expected); + assert_eq!( + mounted + .geometry(&app) + .bounds(crate::allocation_input::CANVAS_ID), + bounds, + "selection feedback must not change the measured canvas rectangle" + ); + assert_eq!( + (app.zoom, app.pan, app.fit_to_view, app.canvas_size), + camera + ); + assert_eq!(app.project(), &baseline.decoded.project); + assert_eq!(app.document.bundle(), &baseline.bundle); + assert!(!app.document.can_undo()); + assert!(!app.draft_dirty()); + } +} diff --git a/crates/oa-desktop/src/app/multiselect.rs b/crates/oa-desktop/src/app/multiselect.rs new file mode 100644 index 00000000..a518b4ae --- /dev/null +++ b/crates/oa-desktop/src/app/multiselect.rs @@ -0,0 +1,433 @@ +//! Exact saved-view selection and atomic group commands. +use super::*; +use crate::diagram_selection::{Context, Scope, Selection, Snapshot}; +use crate::diagram_usage::{Surface, Target}; + +#[derive(Debug, Clone, Copy)] +pub(crate) enum Choice { + Replace, + Add, + Remove, + Toggle, + Inspect, +} + +#[derive(Debug, Clone)] +pub(crate) enum Event { + Choose(Context, Target, Choice), + Collapse(Snapshot, Target), + Clear(Context), + Move(Snapshot, Position), + Hide(Snapshot), + Refuse(Snapshot, String), +} + +pub(crate) fn context(c: crate::route_edit::Context) -> Context { + Context { + scope: Scope { + document: c.document, + surface: Surface::Saved(c.diagram), + }, + revision: c.revision, + epoch: c.epoch, + } +} + +/// The entire typed set must support a placement action. Never filter a mixture. +pub(crate) fn placements(snapshot: &Snapshot) -> Option> { + snapshot.primary()?; + if snapshot.targets().is_empty() { + return None; + } + snapshot + .targets() + .iter() + .map(|target| match target { + Target::Placement(id) => Some(*id), + _ => None, + }) + .collect() +} + +fn visible( + project: &Project, + diagram: Id, + view: &projection::ProjectedView, + target: &Target, +) -> bool { + match target { + Target::Placement(id) => view.nodes.iter().any(|n| n.id == *id), + Target::Port { port, placement } => view + .ports + .iter() + .any(|p| p.element == *port && p.owner == *placement), + Target::Edge(id) => view.edges.iter().any(|e| e.id == *id), + Target::StateRow { + placement, + relation, + } => crate::behavior::state_canvas::visible_row_target( + project, diagram, view, *placement, *relation, + ), + _ => false, + } +} + +impl App { + pub(crate) fn multiselect_boundary(&self) -> impl PartialEq + use<> { + ( + self.canvas_document, + self.document.revision(), + self.page, + self.active_diagram, + self.selected, + self.selected_node, + (self.page == Page::Behavior).then(|| self.state_view_identity()), + (self.page == Page::Behavior).then_some(self.behavior.diagram.placement), + ) + } + pub(crate) fn multiselect_context(&self) -> Option { + match self.page { + Page::Diagram => self.route_context().map(context), + Page::Behavior => { + let current = self.state_context()?; + crate::behavior::state_edit::validate_context(self.project(), current, current) + .ok()?; + Some(context(current)) + } + _ => None, + } + } + + pub(crate) fn multiselect_target_visible(&self, target: &Target) -> bool { + let Some(Context { + scope: + Scope { + surface: Surface::Saved(diagram), + .. + }, + .. + }) = self.multiselect_context() + else { + return false; + }; + let view = projection::project_view_with_index( + self.project(), + diagram, + self.document.view_index(), + ); + visible(self.project(), diagram, &view, target) + } + + pub(crate) fn multiselect_snapshot(&self) -> Option { + self.diagram_selection + .as_ref()? + .snapshot(self.multiselect_context()?) + } + + pub(crate) fn multiselect_envelope(&self, event: &Event) -> bool { + let Some(current) = self.multiselect_context() else { + return false; + }; + match event { + Event::Choose(c, ..) | Event::Clear(c) => *c == current, + Event::Move(s, _) | Event::Hide(s) | Event::Refuse(s, _) | Event::Collapse(s, _) => { + self.diagram_selection + .as_ref() + .is_some_and(|selection| selection.matches(s, current)) + } + } + } + + fn legacy_target(&self, diagram: Id, view: &projection::ProjectedView) -> Option { + let placement = if self.page == Page::Behavior { + self.behavior.diagram.placement + } else { + self.selected_node + }; + if let Some(id) = placement + && view.nodes.iter().any(|node| { + node.id == id && (node.element == self.selected || node.id == self.selected) + }) + { + return Some(Target::Placement(id)); + } + if self.page == Page::Behavior { + // Keep a row's exact occurrence while inspecting its relation. + // Its owner placement is never a node-inspection or Hide target. + if let Some(target @ Target::StateRow { relation, .. }) = + self.diagram_selection.as_ref().and_then(|s| s.primary()) + && *relation == self.selected + && visible(self.project(), diagram, view, target) + { + return Some(target.clone()); + } + if let Some(node) = view.nodes.iter().find(|node| { + crate::behavior::state_canvas::visible_row_target( + self.project(), + diagram, + view, + node.id, + self.selected, + ) + }) { + return Some(Target::StateRow { + placement: node.id, + relation: self.selected, + }); + } + } + if let Some(port) = view.ports.iter().find(|port| port.element == self.selected) { + return Some(Target::Port { + port: port.element, + placement: port.owner, + }); + } + view.edges + .iter() + .any(|edge| edge.id == self.selected) + .then_some(Target::Edge(self.selected)) + } + + pub(crate) fn reconcile_multiselect(&mut self) { + let Some(current) = self.multiselect_context() else { + self.diagram_selection = None; + return; + }; + let Surface::Saved(diagram) = current.scope.surface else { + return; + }; + let view = projection::project_view_with_index( + self.project(), + diagram, + self.document.view_index(), + ); + let legacy = self.legacy_target(diagram, &view); + let lost_primary = self + .diagram_selection + .as_ref() + .filter(|s| s.scope() == current.scope) + .and_then(|s| s.primary()) + .is_some_and(|target| !visible(self.project(), diagram, &view, target)); + let primary_placement = if self.page == Page::Behavior { + self.behavior.diagram.placement + } else { + self.selected_node + }; + let held_primary = self + .diagram_selection + .as_ref() + .filter(|s| s.scope() == current.scope) + .and_then(|s| s.primary()) + .is_some_and(|target| match target { + Target::Placement(id) => primary_placement == Some(*id), + Target::Port { port, .. } => self.selected == *port, + Target::Edge(id) => self.selected == *id, + Target::StateRow { relation, .. } => { + self.selected == *relation && primary_placement.is_none() + } + _ => false, + }); + let project = self.document.project(); + let selection = self + .diagram_selection + .get_or_insert_with(|| Selection::new(current.scope)); + selection.reconcile(current.scope, |target| { + visible(project, diagram, &view, target) + }); + // Navigation/property selection remains explicit. A removed exact target + // is never rebound through its semantic identity to a different placement. + if held_primary || lost_primary { + if selection.primary() != legacy.as_ref() { + self.inspect_multiselect_primary(); + } + } else if selection.primary() != legacy.as_ref() { + if let Some(target) = legacy { + selection.replace(target, |target| visible(project, diagram, &view, target)); + } else { + selection.clear(); + } + } + } + + fn inspect_multiselect_primary(&mut self) { + let diagram = self + .multiselect_context() + .and_then(|c| match c.scope.surface { + Surface::Saved(diagram) => Some(diagram), + _ => None, + }); + let primary = self + .diagram_selection + .as_ref() + .and_then(|s| s.primary()) + .cloned(); + self.selected_node = None; + if self.page == Page::Behavior { + self.behavior.diagram.placement = None; + } + match primary { + Some(Target::Placement(id)) => { + if let Some(node) = diagram + .and_then(|d| self.project().diagrams.get(&d)) + .and_then(|d| d.nodes.get(&id)) + { + self.selected = node.element; + if self.page == Page::Behavior { + self.behavior.diagram.placement = Some(id); + } else { + self.selected_node = Some(id); + } + } + } + Some(Target::Port { port, .. }) => self.selected = port, + Some(Target::Edge(id)) => self.selected = id, + Some(Target::StateRow { relation, .. }) => self.selected = relation, + _ => self.selected = diagram.unwrap_or(self.project().id), + } + self.selected_bend = None; + self.selected_attachment = None; + self.refresh_draft(); + } + + pub(crate) fn multiselect_event(&mut self, event: Event) { + if !self.multiselect_envelope(&event) { + return; + } + if !diagram_removal::enabled(self) + || self.architecture.is_some() + || self.connection.is_some() + || (self.page == Page::Behavior + && self.behavior.diagram.tool != crate::behavior::state_edit::Tool::Select) + { + self.status = + "Finish or cancel the active edit before changing the diagram selection.".into(); + return; + } + let current = self.multiselect_context().unwrap(); + let Surface::Saved(diagram) = current.scope.surface else { + return; + }; + let view = projection::project_view_with_index( + self.project(), + diagram, + self.document.view_index(), + ); + match event { + Event::Collapse(_, target) => { + if !visible(self.project(), diagram, &view, &target) { + return; + } + let project = self.document.project(); + self.diagram_selection + .as_mut() + .unwrap() + .replace(target, |target| visible(project, diagram, &view, target)); + self.inspect_multiselect_primary(); + } + Event::Choose(_, target, choice) => { + if !visible(self.project(), diagram, &view, &target) { + return; + } + self.reconcile_multiselect(); + let project = self.document.project(); + let selection = self.diagram_selection.as_mut().unwrap(); + let valid = |target: &Target| visible(project, diagram, &view, target); + match choice { + Choice::Replace => { + selection.replace(target, valid); + } + Choice::Add => { + selection.add(target, valid); + } + Choice::Remove => { + selection.remove(&target); + } + Choice::Toggle => { + selection.toggle(target, valid); + } + Choice::Inspect => { + selection.set_primary(&target); + } + } + self.inspect_multiselect_primary(); + } + Event::Clear(_) => { + if let Some(selection) = &mut self.diagram_selection { + selection.clear(); + } + self.inspect_multiselect_primary(); + } + Event::Refuse(_, reason) => self.status = reason, + Event::Move(snapshot, delta) => { + let Some(ids) = placements(&snapshot) else { + self.status = "Move requires only placed elements. Remove ports, rows and connections from the selection.".into(); + return; + }; + if !snapshot + .targets() + .iter() + .all(|target| visible(self.project(), diagram, &view, target)) + { + return; + } + if self.project().diagrams[&diagram].projection == ProjectionKind::Allocation + && ids.iter().any(|id| { + self.project().elements + [&self.project().diagrams[&diagram].nodes[id].element] + .kind() + .is_work() + }) + { + self.status = + "Group allocation drag is not available yet. The selection was retained." + .into(); + return; + } + if delta.x == 0. && delta.y == 0. { + return; + } + match oa_core::diagram_edit::translate_placements( + self.project(), + diagram, + &ids, + delta, + ) { + Ok(command) => { + if self.execute(command) { + self.inspect_multiselect_primary(); + self.status = format!( + "Moved {} selected elements · Undo restores the group", + ids.len() + ); + } + } + Err(error) => self.status = error.to_string(), + } + } + Event::Hide(snapshot) => { + let Some(ids) = placements(&snapshot) else { + self.status = + "Hide requires only placed elements. Model objects remain unchanged." + .into(); + return; + }; + if !snapshot + .targets() + .iter() + .all(|target| visible(self.project(), diagram, &view, target)) + { + return; + } + if self.execute(Command::Batch( + ids.iter().copied().map(Command::Delete).collect(), + )) { + if let Some(selection) = &mut self.diagram_selection { + selection.clear(); + } + self.inspect_multiselect_primary(); + self.status = format!("Hid {} placements · Model objects retained", ids.len()); + } + } + } + } +} diff --git a/crates/oa-desktop/src/app/multiselect_tests.rs b/crates/oa-desktop/src/app/multiselect_tests.rs new file mode 100644 index 00000000..d0d06012 --- /dev/null +++ b/crates/oa-desktop/src/app/multiselect_tests.rs @@ -0,0 +1,602 @@ +//! Saved-view group operations through the real App message boundary. +use super::multiselect::{Choice as GroupChoice, Event as GroupEvent}; +use super::*; +use crate::diagram_usage::Target; + +fn point(x: f32, y: f32) -> Position { + Position { x, y } +} + +struct Fixture { + app: App, + diagram: Id, + a: Id, + b: Id, + child: Id, + edge: Id, +} + +fn fixture() -> Fixture { + let mut p = Project::empty(); + let a = p + .add_element(Kind::Function, Layer::Sa, "Send", None) + .unwrap(); + let b = p + .add_element(Kind::Function, Layer::Sa, "Receive", None) + .unwrap(); + let child = p + .add_element(Kind::Function, Layer::Sa, "Prepare", Some(a)) + .unwrap(); + let edge = p.relate(RelationKind::FunctionalExchange, "Signal", a, b); + let diagram = p.ensure_diagram(Layer::Sa); + p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::FunctionalDataflow; + let a = p.place(diagram, a, point(10., 20.)); + let b = p.place(diagram, b, point(500., 20.)); + let child = p.place(diagram, child, point(40., 100.)); + p.diagrams + .get_mut(&diagram) + .unwrap() + .routes + .push(EdgeRoute { + edge, + source_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Bottom, + offset: 0.3, + }), + target_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Top, + offset: 0.7, + }), + waypoints: vec![point(370., 150.)], + label: Some(EdgeLabel { + fraction: 0.4, + offset: point(5., -9.), + }), + }); + let other = p + .add_element(Kind::Activity, Layer::Oa, "Operational work", None) + .unwrap(); + let other_diagram = p.ensure_diagram(Layer::Oa); + p.place(other_diagram, other, point(-50., -100.)); + let mut app = App::with_project(p); + app.page = Page::Diagram; + app.layer = Layer::Sa; + app.active_diagram = Some(diagram); + app.selected = diagram; + app.selected_node = None; + app.refresh_draft(); + app.reconcile_multiselect(); + Fixture { + app, + diagram, + a, + b, + child, + edge, + } +} + +fn dispatch(app: &mut App, event: GroupEvent) { + let _ = app.update(Message::DiagramSelection(event)); +} +fn choose(app: &mut App, target: Target, choice: GroupChoice) { + let context = app.multiselect_context().unwrap(); + dispatch(app, GroupEvent::Choose(context, target, choice)); +} +fn select_pair(f: &mut Fixture) { + choose(&mut f.app, Target::Placement(f.a), GroupChoice::Replace); + choose(&mut f.app, Target::Placement(f.b), GroupChoice::Toggle); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a), Target::Placement(f.b)] + ); +} + +#[test] +fn queued_singleton_move_or_delete_cannot_partially_edit_a_new_group() { + for model in [false, true] { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let selected = f.app.multiselect_snapshot().unwrap(); + let object = f.app.project().diagrams[&f.diagram].nodes[&f.b].element; + for message in [ + Message::MoveNode(f.b, point(700., 600.)), + Message::DiagramRemove(super::diagram_removal::Intent { + context: f.app.route_context().unwrap(), + object, + placement: Some(f.b), + model, + }), + Message::Delete, + ] { + let _ = f.app.update(message); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selected.targets() + ); + assert!(!f.app.delete_pending); + assert!(!f.app.document.can_undo()); + } + } +} + +#[test] +fn legacy_hide_action_applies_to_the_complete_visible_group() { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.remove(&f.a); + view.nodes.remove(&f.b); + let _ = f.app.update(Message::HideSelected); + assert_eq!(f.app.project(), &expected); + assert_source_matches_model(&f.app); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); +} +fn assert_source_matches_model(app: &App) { + let compiled = oa_core::source_project::compile_bundle(app.document.bundle(), LIMITS).unwrap(); + assert_eq!(&compiled.decoded.project, app.project()); + assert_eq!(&compiled.bundle, app.document.bundle()); +} + +#[test] +fn toggled_group_moves_model_and_source_once_and_keeps_primary_through_history() { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let snapshot = f.app.multiselect_snapshot().unwrap(); + assert_eq!(snapshot.primary(), Some(&Target::Placement(f.b))); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.get_mut(&f.a).unwrap().position = point(50., 80.); + view.nodes.get_mut(&f.b).unwrap().position = point(540., 80.); + view.nodes.get_mut(&f.child).unwrap().position = point(80., 160.); + view.routes[0].waypoints = vec![point(410., 210.)]; + dispatch( + &mut f.app, + GroupEvent::Move(snapshot.clone(), point(40., 60.)), + ); + assert_eq!(f.app.project(), &expected); + assert_source_matches_model(&f.app); + assert_eq!(f.app.selected_node, Some(f.b)); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + snapshot.targets() + ); + let after = f.app.document.snapshot(); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + let _ = f.app.update(Message::Redo); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &after.bundle); + assert!(!f.app.document.can_redo()); + assert_eq!(f.app.selected_node, Some(f.b)); +} + +#[test] +fn ancestor_and_child_selection_moves_child_exactly_once() { + let mut f = fixture(); + choose(&mut f.app, Target::Placement(f.a), GroupChoice::Replace); + choose(&mut f.app, Target::Placement(f.child), GroupChoice::Toggle); + let snapshot = f.app.multiselect_snapshot().unwrap(); + assert_eq!(snapshot.targets().len(), 2); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.get_mut(&f.a).unwrap().position = point(0., 45.); + view.nodes.get_mut(&f.child).unwrap().position = point(30., 125.); + dispatch(&mut f.app, GroupEvent::Move(snapshot, point(-10., 25.))); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.selected_node, Some(f.child)); + assert_source_matches_model(&f.app); +} + +#[test] +fn mixed_edge_or_port_selection_refuses_the_entire_move() { + for include_port in [false, true] { + let mut f = fixture(); + choose(&mut f.app, Target::Placement(f.a), GroupChoice::Replace); + let extra = if include_port { + let view = projection::project_view(f.app.project(), f.diagram); + let port = view + .ports + .iter() + .find(|p| p.owner == f.a) + .expect("functional exchange exposes its typed owner port"); + Target::Port { + port: port.element, + placement: port.owner, + } + } else { + Target::Edge(f.edge) + }; + choose(&mut f.app, extra.clone(), GroupChoice::Toggle); + let selection = f.app.multiselect_snapshot().unwrap(); + assert_eq!(selection.targets(), &[Target::Placement(f.a), extra]); + let before = f.app.document.snapshot(); + let revision = f.app.document.revision(); + dispatch( + &mut f.app, + GroupEvent::Move(selection.clone(), point(40., 60.)), + ); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.document.revision(), revision); + assert!(!f.app.document.can_undo()); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selection.targets() + ); + assert!(f.app.status.contains("Move requires only placed elements")); + } +} + +#[test] +fn stale_operation_envelopes_have_no_selection_history_status_or_allocation_side_effects() { + // Isolate each lifetime component, including the same visible set recreated + // by a remove/re-add cycle. Accepted setup changes are outside the assertion. + for stale_by in ["epoch", "revision", "document", "selection generation"] { + for action in ["move", "hide", "refuse", "collapse"] { + let mut f = fixture(); + select_pair(&mut f); + let stale = f.app.multiselect_snapshot().unwrap(); + match stale_by { + "epoch" => f.app.canvas_epoch += 1, + "revision" => { + let semantic = f.app.project().diagrams[&f.diagram].nodes[&f.a].element; + // Advance only the document revision, retaining this canvas + // and selection epoch to independently test revision checking. + f.app + .document + .execute(Command::Describe(semantic, "New revision".into())) + .unwrap(); + } + "document" => f.app.canvas_document = Id::new_v4(), + _ => { + choose(&mut f.app, Target::Placement(f.b), GroupChoice::Toggle); + choose(&mut f.app, Target::Placement(f.b), GroupChoice::Toggle); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + stale.targets() + ); + } + } + f.app.status = "Current feedback must survive".into(); + f.app.allocation_drag.arrange_next = true; + let before = f.app.document.snapshot(); + let selection = f.app.multiselect_snapshot(); + let legacy = (f.app.selected, f.app.selected_node); + let revision = f.app.document.revision(); + let epochs = ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch, + ); + let history = (f.app.document.can_undo(), f.app.document.can_redo()); + let draft = f + .app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); + let event = match action { + "move" => GroupEvent::Move(stale, point(99., 100.)), + "hide" => GroupEvent::Hide(stale), + "collapse" => GroupEvent::Collapse(stale, Target::Placement(f.a)), + _ => GroupEvent::Refuse(stale, "Obsolete refusal".into()), + }; + dispatch(&mut f.app, event); + assert_eq!( + f.app.project(), + &before.decoded.project, + "{stale_by}/{action}" + ); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.document.revision(), revision); + assert_eq!(f.app.multiselect_snapshot(), selection); + assert_eq!((f.app.selected, f.app.selected_node), legacy); + assert_eq!( + ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch + ), + epochs + ); + assert_eq!( + (f.app.document.can_undo(), f.app.document.can_redo()), + history + ); + assert_eq!(f.app.status, "Current feedback must survive"); + assert!(f.app.allocation_drag.arrange_next); + assert_eq!( + f.app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), + draft + ); + } + } +} + +#[test] +fn pending_valid_and_invalid_property_drafts_refuse_group_edits_without_applying_them() { + for (path, value) in [("/name", "Unapplied name"), ("/id", "-")] { + for hide in [false, true] { + let mut f = fixture(); + select_pair(&mut f); + let _ = f.app.update(Message::Field(path.into(), value.into())); + assert!(f.app.draft_dirty()); + let snapshot = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + let draft = + serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + let event = if hide { + GroupEvent::Hide(snapshot.clone()) + } else { + GroupEvent::Move(snapshot.clone(), point(20., 30.)) + }; + dispatch(&mut f.app, event); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + snapshot.targets() + ); + assert_eq!( + serde_json::to_value(f.app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + draft + ); + assert!(f.app.status.contains("Finish or cancel")); + } + } +} + +#[test] +fn hiding_a_group_removes_only_exact_placements_and_roundtrips_in_one_history_step() { + let mut f = fixture(); + select_pair(&mut f); + let snapshot = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.remove(&f.a); + view.nodes.remove(&f.b); + dispatch(&mut f.app, GroupEvent::Hide(snapshot)); + assert_eq!( + f.app.project(), + &expected, + "engineering objects, descendant placement, authored route and opposite-layer view survive" + ); + assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty()); + assert_source_matches_model(&f.app); + let after = f.app.document.snapshot(); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + let _ = f.app.update(Message::Redo); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &after.bundle); + assert!(!f.app.document.can_redo()); +} +fn assert_surviving_primary(f: &Fixture) { + let selection = f.app.multiselect_snapshot().unwrap(); + assert_eq!(selection.targets(), &[Target::Placement(f.a)]); + assert_eq!(selection.primary(), Some(&Target::Placement(f.a))); + assert_eq!(f.app.selected_node, Some(f.a)); + assert_eq!( + f.app.selected, + f.app.project().diagrams[&f.diagram].nodes[&f.a].element + ); +} + +#[test] +fn removing_primary_placement_from_model_preserves_exact_survivor_after_undo() { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let _ = f.app.update(Message::Command(Command::Delete(f.b))); + assert!( + !f.app.project().diagrams[&f.diagram] + .nodes + .contains_key(&f.b) + ); + assert_surviving_primary(&f); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_surviving_primary(&f); + let _ = f.app.update(Message::Redo); + assert_surviving_primary(&f); +} + +#[test] +fn deleting_primary_model_object_then_undo_does_not_restore_removed_selection_membership() { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let semantic = f.app.project().diagrams[&f.diagram].nodes[&f.b].element; + let _ = f.app.update(Message::Command(Command::Delete(semantic))); + assert!(!f.app.project().elements.contains_key(&semantic)); + assert_surviving_primary(&f); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_surviving_primary(&f); + let _ = f.app.update(Message::Redo); + assert_surviving_primary(&f); +} + +#[test] +fn replacing_primary_occurrence_with_same_semantic_placement_does_not_retarget_selection() { + let mut f = fixture(); + select_pair(&mut f); + let mut candidate = f.app.project().clone(); + let view = candidate.diagrams.get_mut(&f.diagram).unwrap(); + let mut replacement = view.nodes.remove(&f.b).unwrap(); + let replacement_id = Id::new_v4(); + replacement.id = replacement_id; + replacement.alias = "replacement_receive".into(); + replacement.position = point(800., 300.); + view.nodes.insert(replacement_id, replacement); + // Replace preserves the semantic inspector identity. Its normal sync used + // to rediscover the new occurrence through that identity and steal the set. + let _ = f + .app + .update(Message::Command(Command::Replace(candidate.clone()))); + assert_eq!(f.app.project(), &candidate); + assert_surviving_primary(&f); + assert!( + !f.app + .multiselect_snapshot() + .unwrap() + .targets() + .contains(&Target::Placement(replacement_id)) + ); + assert_source_matches_model(&f.app); + let _ = f.app.update(Message::Undo); + assert!( + f.app.project().diagrams[&f.diagram] + .nodes + .contains_key(&f.b) + ); + assert_surviving_primary(&f); +} + +#[test] +fn explicit_surface_navigation_and_return_never_resurrect_the_group() { + for destination in [Page::Catalog, Page::Behavior] { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let stale = f.app.multiselect_snapshot().unwrap(); + let _ = f.app.update(Message::Page(destination)); + assert!(f.app.multiselect_snapshot().is_none()); + let _ = f.app.update(Message::Page(Page::Diagram)); + let live = f.app.multiselect_snapshot().unwrap(); + assert!( + live.targets().len() <= 1, + "navigation may inspect one exact member but must not restore the group" + ); + dispatch(&mut f.app, GroupEvent::Move(stale, point(400., 500.))); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + } +} + +fn open_placement_menu(f: &mut Fixture, placement: Id) { + let semantic = f.app.project().diagrams[&f.diagram].nodes[&placement].element; + let mut request = crate::model_menu::Request::object(semantic, iced::Point::new(100., 100.)); + request.placement = Some(placement); + request.canvas = f.app.route_context(); + let _ = f + .app + .update(Message::Menu(crate::model_menu::Event::Open(request))); + assert!(f.app.model_menu.is_some()); +} + +fn menu_action(app: &App, caption: &str) -> Message { + crate::model_menu::popup(app) + .unwrap() + .entries + .into_iter() + .find_map(|entry| match entry { + crate::context_menu::Entry::Action { label, message, .. } if label == caption => { + message + } + _ => None, + }) + .unwrap_or_else(|| panic!("enabled menu action {caption:?} must exist")) +} + +#[test] +fn context_menu_dismissal_preserves_group_and_add_keeps_pre_menu_members() { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let selected = f.app.multiselect_snapshot().unwrap(); + let child = f.child; + open_placement_menu(&mut f, child); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selected.targets() + ); + assert_eq!( + f.app.multiselect_snapshot().unwrap().primary(), + selected.primary() + ); + let _ = f.app.update(Message::Menu(crate::model_menu::Event::Close)); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selected.targets() + ); + assert_eq!( + f.app.multiselect_snapshot().unwrap().primary(), + selected.primary() + ); + open_placement_menu(&mut f, child); + let add = menu_action(&f.app, "Add to selection"); + let _ = f.app.update(add); + let selected = f.app.multiselect_snapshot().unwrap(); + assert_eq!( + selected.targets(), + &[ + Target::Placement(f.a), + Target::Placement(f.b), + Target::Placement(child) + ] + ); + assert_eq!(selected.primary(), Some(&Target::Placement(child))); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); +} + +#[test] +fn selected_member_context_menu_exposes_and_executes_group_hide_not_singleton_deletion() { + let mut f = fixture(); + select_pair(&mut f); + let before = f.app.document.snapshot(); + let member = f.a; + open_placement_menu(&mut f, member); + let popup = crate::model_menu::popup(&f.app).unwrap(); + for entry in &popup.entries { + if let crate::context_menu::Entry::Action { + message: Some(Message::Menu(crate::model_menu::Event::Invoke(_, action))), + .. + } = entry + { + assert!( + !matches!( + action.as_ref(), + Message::HideSelected | Message::Delete | Message::DiagramRemove(_) + ), + "a selected-member group menu must not expose singleton removal" + ); + } + } + let hide = menu_action(&f.app, "Hide selected placements"); + let _ = f.app.update(hide); + let mut expected = before.decoded.project.clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.remove(&f.a); + view.nodes.remove(&f.b); + assert_eq!(f.app.project(), &expected); + assert!(!f.app.delete_pending); + assert!(f.app.model_menu.is_none()); + assert_source_matches_model(&f.app); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); +} diff --git a/crates/oa-desktop/src/app/state_attachment_escape_tests.rs b/crates/oa-desktop/src/app/state_attachment_escape_tests.rs index 2f997ee7..6396144e 100644 --- a/crates/oa-desktop/src/app/state_attachment_escape_tests.rs +++ b/crates/oa-desktop/src/app/state_attachment_escape_tests.rs @@ -53,6 +53,7 @@ async fn armed() -> (App, Mounted, Id) { context, object: relation, placement: None, + selection_target: None, anchor: iced::Point::new(450., 400.), bend: None, route_segment: None, diff --git a/crates/oa-desktop/src/app/state_bend_tests.rs b/crates/oa-desktop/src/app/state_bend_tests.rs index 502ac5d0..47128291 100644 --- a/crates/oa-desktop/src/app/state_bend_tests.rs +++ b/crates/oa-desktop/src/app/state_bend_tests.rs @@ -79,6 +79,7 @@ fn state_bend_menu_carries_exact_segment_and_pin_without_changing_delete_action( context, object: relation, placement: None, + selection_target: None, anchor: iced::Point::ORIGIN, bend: Some(1), route_segment: Some((2, point)), diff --git a/crates/oa-desktop/src/app/state_diagram.rs b/crates/oa-desktop/src/app/state_diagram.rs index 49440e78..e484b077 100644 --- a/crates/oa-desktop/src/app/state_diagram.rs +++ b/crates/oa-desktop/src/app/state_diagram.rs @@ -115,6 +115,7 @@ impl App { } } match event { + Event::Selection(event) => self.multiselect_event(event), Event::Remove(intent) => self.remove_from_diagram(intent), Event::Viewport(_, size) => { if size.width.is_finite() @@ -157,6 +158,7 @@ impl App { context, object, placement, + selection_target, anchor, bend, route_segment, @@ -164,6 +166,7 @@ impl App { let mut request = crate::model_menu::Request::object(object, anchor); request.canvas = Some(context); request.placement = placement; + request.selection_target = selection_target.map(Box::new); request.bend = bend; request.route_segment = route_segment; return self.handle(Message::Menu(crate::model_menu::Event::Open(request))); diff --git a/crates/oa-desktop/src/app/state_multiselect_focus_tests.rs b/crates/oa-desktop/src/app/state_multiselect_focus_tests.rs new file mode 100644 index 00000000..059c5d5a --- /dev/null +++ b/crates/oa-desktop/src/app/state_multiselect_focus_tests.rs @@ -0,0 +1,374 @@ +//! State selection layout and modifier admission on the mounted production App. +use super::*; +use crate::behavior::{ + canvas_workspace::tests::{Mounted, key}, + state_edit, +}; +use crate::diagram_usage::Target; +use iced::advanced::widget::{Operation, operation}; +use iced::{Point, Rectangle, Size, Vector, keyboard, mouse}; + +struct Fixture { + app: App, + diagram: Id, + a: Id, + b: Id, + a_element: Id, + b_element: Id, + edge: Id, + action: Id, +} + +fn fixture() -> Fixture { + let mut p = Project::empty(); + let machine = p + .add_element(Kind::StateMachine, Layer::Oa, "Operating modes", None) + .unwrap(); + let region = p + .add_element(Kind::Region, Layer::Oa, "Main region", Some(machine)) + .unwrap(); + let a_element = p + .add_element(Kind::State, Layer::Oa, "First state", Some(region)) + .unwrap(); + let b_element = p + .add_element( + Kind::State, + Layer::Oa, + &format!("Second state {}", "with a long primary name ".repeat(20)), + Some(region), + ) + .unwrap(); + let work = p + .add_element(Kind::Activity, Layer::Oa, "Reusable work", None) + .unwrap(); + let action = p.relate(RelationKind::StateAction, "On entry", a_element, work); + let edge = p.relate( + RelationKind::StateTransition, + "Next state", + a_element, + b_element, + ); + let diagram = p.ensure_diagram(Layer::Oa); + p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::StateMachine { machine }; + let mut placements = vec![]; + // All State elements, including machine/Region, are initially placed. + // A fixture already missing its containers would not test picker insertion. + for (element, x, y, width, height) in [ + (machine, 0., 0., 800., 500.), + (region, 20., 70., 760., 410.), + (a_element, 60., 160., 230., 180.), + (b_element, 430., 160., 260., 180.), + ] { + let node = p.place(diagram, element, Position { x, y }); + p.diagrams + .get_mut(&diagram) + .unwrap() + .nodes + .get_mut(&node) + .unwrap() + .size = oa_core::Size { width, height }; + placements.push(node); + } + let a = placements[2]; + let b = placements[3]; + validate_candidate(&p).unwrap(); + assert!(state_edit::unplaced(&p, diagram).is_empty()); + let mut app = App::with_project(p); + app.page = Page::Behavior; + app.layer = Layer::Oa; + app.behavior.active_owner = Some(machine); + app.behavior.diagram = crate::behavior::state_view::State { + diagram: Some(diagram), + fit: true, + ..Default::default() + }; + app.selected = diagram; + app.selected_node = None; + app.window_size = Size::new(1442., 932.); + app.refresh_draft(); + app.reconcile_multiselect(); + assert_eq!(app.state_context().unwrap().diagram, diagram); + Fixture { + app, + diagram, + a, + b, + a_element, + b_element, + edge, + action, + } +} + +fn owner(mounted: &mut Mounted, app: &App) -> (Rectangle, bool) { + let geometry = mounted.geometry(app); + let (_, bounds, focused) = geometry + .focused + .into_iter() + .find(|(id, _, _)| { + id.as_ref() == Some(&iced::advanced::widget::Id::from("diagram-keyboard-owner")) + }) + .expect("real State focus::Owner rectangle must be measurable"); + (bounds, focused) +} +fn camera(app: &App) -> (f32, Vector, bool, Option) { + let state = &app.behavior.diagram; + (state.zoom, state.pan, state.fit, state.viewport) +} +async fn selection(mounted: &mut Mounted, app: &mut App, target: Option) { + let context = app.multiselect_context().unwrap(); + let event = match target { + Some(target) => multiselect::Event::Choose(context, target, multiselect::Choice::Toggle), + None => multiselect::Event::Clear(context), + }; + let task = app.update(Message::DiagramSelection(event)); + mounted.perform(app, task).await; + mounted.settle(app).await; +} +fn node_point(mounted: &mut Mounted, f: &Fixture, placement: Id) -> Point { + let bounds = owner(mounted, &f.app).0; + let node = &f.app.project().diagrams[&f.diagram].nodes[&placement]; + let state = &f.app.behavior.diagram; + Point::new( + bounds.x + (node.position.x + 40.) * state.zoom + state.pan.x, + bounds.y + (node.position.y + 25.) * state.zoom + state.pan.y, + ) +} +fn modifiers(shift: bool) -> iced::Event { + iced::Event::Keyboard(keyboard::Event::ModifiersChanged(if shift { + keyboard::Modifiers::SHIFT + } else { + keyboard::Modifiers::default() + })) +} +struct FocusField(Rectangle); +impl Operation for FocusField { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) { + visit(self); + } + fn focusable( + &mut self, + _: Option<&iced::advanced::widget::Id>, + bounds: Rectangle, + state: &mut dyn operation::Focusable, + ) { + if bounds == self.0 { + state.focus(); + } else { + state.unfocus(); + } + } +} + +#[tokio::test] +async fn mounted_state_selection_and_missing_picker_never_resize_or_refit_the_canvas() { + let mut f = fixture(); + let mut mounted = Mounted::new(&mut f.app).await; + let baseline = f.app.document.snapshot(); + let bounds = owner(&mut mounted, &f.app).0; + let fitted = camera(&f.app); + assert!( + fitted.2, + "keep Fit active to expose any resize-triggered camera jump" + ); + assert!(fitted.3.is_some()); + assert!(bounds.width > 400. && bounds.height > 200.); + assert!(state_edit::unplaced(f.app.project(), f.diagram).is_empty()); + assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty()); + for (target, expected) in [ + (Some(Target::Placement(f.a)), vec![Target::Placement(f.a)]), + ( + Some(Target::Placement(f.b)), + vec![Target::Placement(f.a), Target::Placement(f.b)], + ), + ( + Some(Target::Edge(f.edge)), + vec![ + Target::Placement(f.a), + Target::Placement(f.b), + Target::Edge(f.edge), + ], + ), + ( + Some(Target::Edge(f.edge)), + vec![Target::Placement(f.a), Target::Placement(f.b)], + ), + ( + Some(Target::StateRow { + placement: f.a, + relation: f.action, + }), + vec![ + Target::Placement(f.a), + Target::Placement(f.b), + Target::StateRow { + placement: f.a, + relation: f.action, + }, + ], + ), + (None, vec![]), + ] { + selection(&mut mounted, &mut f.app, target).await; + assert_eq!(f.app.multiselect_snapshot().unwrap().targets(), expected); + assert_eq!( + owner(&mut mounted, &f.app).0, + bounds, + "selection strip changed the measured State canvas rectangle" + ); + assert_eq!( + camera(&f.app), + fitted, + "selection feedback silently refitted the camera" + ); + assert_eq!(f.app.project(), &baseline.decoded.project); + assert_eq!(f.app.document.bundle(), &baseline.bundle); + assert!(!f.app.document.can_undo()); + assert!(!f.app.draft_dirty()); + } + selection(&mut mounted, &mut f.app, Some(Target::Placement(f.a))).await; + selection(&mut mounted, &mut f.app, Some(Target::Placement(f.b))).await; + let captured = f.app.multiselect_snapshot().unwrap(); + let task = f + .app + .update(Message::DiagramSelection(multiselect::Event::Hide( + captured, + ))); + mounted.perform(&mut f.app, task).await; + mounted.settle(&mut f.app).await; + let missing: std::collections::BTreeSet<_> = state_edit::unplaced(f.app.project(), f.diagram) + .into_iter() + .collect(); + assert_eq!( + missing, + std::collections::BTreeSet::from([f.a_element, f.b_element]) + ); + let mut expected = baseline.decoded.project.clone(); + expected + .diagrams + .get_mut(&f.diagram) + .unwrap() + .nodes + .remove(&f.a); + expected + .diagrams + .get_mut(&f.diagram) + .unwrap() + .nodes + .remove(&f.b); + assert_eq!(f.app.project(), &expected); + assert_eq!( + owner(&mut mounted, &f.app).0, + bounds, + "the first unplaced picker changed canvas bounds" + ); + assert_eq!(camera(&f.app), fitted); + let hidden_source = f.app.document.bundle().clone(); + let task = f.app.update(Message::Undo); + mounted.perform(&mut f.app, task).await; + mounted.settle(&mut f.app).await; + assert!(state_edit::unplaced(f.app.project(), f.diagram).is_empty()); + assert_eq!(f.app.project(), &baseline.decoded.project); + assert_eq!(f.app.document.bundle(), &baseline.bundle); + assert!(!f.app.document.can_undo()); + assert_eq!( + owner(&mut mounted, &f.app).0, + bounds, + "removing the unplaced picker changed canvas bounds" + ); + assert_eq!(camera(&f.app), fitted); + let task = f.app.update(Message::Redo); + mounted.perform(&mut f.app, task).await; + mounted.settle(&mut f.app).await; + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &hidden_source); + assert_eq!(owner(&mut mounted, &f.app).0, bounds); + assert_eq!(camera(&f.app), fitted); +} + +#[tokio::test] +async fn mounted_state_shift_held_in_clean_inspector_toggles_on_first_canvas_press() { + let mut f = fixture(); + let mut mounted = Mounted::new(&mut f.app).await; + selection(&mut mounted, &mut f.app, Some(Target::Placement(f.a))).await; + let field = mounted + .geometry(&f.app) + .text_inputs + .into_iter() + .filter(|bounds| bounds.x > f.app.window_size.width - 320.) + .min_by(|a, b| a.y.total_cmp(&b.y)) + .expect("State Inspector field is mounted"); + mounted.operate(&f.app, &mut FocusField(field)); + assert!(!owner(&mut mounted, &f.app).1); + let before = f.app.document.snapshot(); + mounted + .batch(&mut f.app, &[modifiers(true)], mouse::Cursor::Unavailable) + .await; + assert!( + !owner(&mut mounted, &f.app).1, + "passive Shift must not acquire State canvas focus" + ); + let at = node_point(&mut mounted, &f, f.b); + let messages = mounted.click(&mut f.app, at).await; + assert!(messages.iter().any(|message| matches!(message, + Message::DiagramSelection(multiselect::Event::Choose(_, Target::Placement(id), multiselect::Choice::Toggle)) if *id == f.b + )), "StateCanvas must map its typed selection event to the common App boundary"); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a), Target::Placement(f.b)] + ); + assert_eq!(f.app.behavior.diagram.placement, Some(f.b)); + assert_eq!(f.app.selected_node, None); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + assert!(!f.app.draft_dirty()); +} + +#[tokio::test] +async fn mounted_state_busy_suspension_tracks_shift_without_granting_focus_or_commands() { + let mut f = fixture(); + let mut mounted = Mounted::new(&mut f.app).await; + selection(&mut mounted, &mut f.app, Some(Target::Placement(f.a))).await; + // Explicitly revoke focus rather than relying on incidental initial focus. + mounted.operate( + &f.app, + &mut FocusField(Rectangle { + x: -1000., + y: -1000., + width: 1., + height: 1., + }), + ); + let before = f.app.document.snapshot(); + let at = node_point(&mut mounted, &f, f.b); + f.app.busy = true; + mounted.settle(&mut f.app).await; + mounted + .batch( + &mut f.app, + &[modifiers(true), key(keyboard::key::Named::Delete)], + mouse::Cursor::Unavailable, + ) + .await; + mounted.click(&mut f.app, at).await; + assert!(!owner(&mut mounted, &f.app).1); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a)] + ); + assert_eq!(f.app.document.bundle(), &before.bundle); + f.app.busy = false; + mounted.settle(&mut f.app).await; + assert!(!owner(&mut mounted, &f.app).1); + // No second modifier event: the passive suspended update must survive. + mounted.click(&mut f.app, at).await; + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a), Target::Placement(f.b)] + ); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + assert!(!f.app.delete_pending); +} diff --git a/crates/oa-desktop/src/app/state_multiselect_tests.rs b/crates/oa-desktop/src/app/state_multiselect_tests.rs new file mode 100644 index 00000000..4fde933e --- /dev/null +++ b/crates/oa-desktop/src/app/state_multiselect_tests.rs @@ -0,0 +1,1092 @@ +//! Persisted State groups through the real App transaction/admission boundary. +use super::multiselect::{Choice as SelectionChoice, Event as SelectionEvent}; +use super::*; +use crate::behavior::{ + state_edit::{Event as StateEvent, Tool}, + states, +}; +use crate::diagram_usage::Target; + +fn point(x: f32, y: f32) -> Position { + Position { x, y } +} + +struct Fixture { + app: App, + diagram: Id, + other_view: Id, + other_machine: Id, + machine: Id, + a: Id, + b: Id, + child: Id, + a_element: Id, + b_element: Id, + internal: Id, + action: Id, + internal_row: Id, +} + +fn route(edge: Id, pins: Vec) -> EdgeRoute { + EdgeRoute { + edge, + source_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Bottom, + offset: 0.3, + }), + target_attachment: Some(EdgeAttachment { + side: PortAnchorSide::Top, + offset: 0.7, + }), + waypoints: pins, + label: Some(EdgeLabel { + fraction: 0.4, + offset: point(5., -9.), + }), + } +} + +fn fixture() -> Fixture { + let mut p = Project::empty(); + let system = p + .add_element(Kind::System, Layer::Sa, "System", None) + .unwrap(); + let machine = p + .add_element( + Kind::StateMachine, + Layer::Sa, + "Operating modes", + Some(system), + ) + .unwrap(); + let region = p + .add_element(Kind::Region, Layer::Sa, "Main region", Some(machine)) + .unwrap(); + let a_element = p + .add_element(Kind::State, Layer::Sa, "Operating", Some(region)) + .unwrap(); + let b_element = p + .add_element(Kind::State, Layer::Sa, "Standby", Some(region)) + .unwrap(); + let c_element = p + .add_element(Kind::State, Layer::Sa, "Stopped", Some(region)) + .unwrap(); + let inner = p + .add_element(Kind::Region, Layer::Sa, "Nested region", Some(a_element)) + .unwrap(); + let child_element = p + .add_element(Kind::State, Layer::Sa, "Working", Some(inner)) + .unwrap(); + let work = p + .add_element(Kind::Function, Layer::Sa, "Reusable work", None) + .unwrap(); + let action = p.relate(RelationKind::StateAction, "Entry work", a_element, work); + // The row references shared work, not ownership or a placed work target. + let other_action = p.relate( + RelationKind::StateAction, + "Other exit work", + b_element, + work, + ); + if let RelationBody::StateAction { phase, .. } = + &mut p.relations.get_mut(&other_action).unwrap().body + { + *phase = StateActionKind::Exit; + } + let internal_row = p.relate( + RelationKind::StateTransition, + "Internal check", + a_element, + a_element, + ); + if let RelationBody::StateTransition { + transition_kind, .. + } = &mut p.relations.get_mut(&internal_row).unwrap().body + { + *transition_kind = TransitionKind::Internal; + } + let internal = p.relate( + RelationKind::StateTransition, + "Leave operating", + a_element, + b_element, + ); + if let RelationBody::StateTransition { + guard, + trigger, + effect, + .. + } = &mut p.relations.get_mut(&internal).unwrap().body + { + *guard = "temperature nominal\nand pressure stable".into(); + *trigger = "operator request".into(); + *effect = "record transition".into(); + } + let external = p.relate(RelationKind::StateTransition, "Stop", b_element, c_element); + let diagram = p.ensure_diagram(Layer::Sa); + p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::StateMachine { machine }; + let a = p.place(diagram, a_element, point(10.25, 20.5)); + let b = p.place(diagram, b_element, point(510.5, 40.25)); + p.place(diagram, c_element, point(900., 400.)); + let child = p.place(diagram, child_element, point(80.5, 240.25)); + let view = p.diagrams.get_mut(&diagram).unwrap(); + view.nodes.get_mut(&a).unwrap().size = oa_core::Size { + width: 360., + height: 420., + }; + view.routes = vec![ + route(internal, vec![point(450.25, 150.5)]), + route(external, vec![point(780.5, 300.25)]), + ]; + let other_view = p.ensure_diagram(Layer::Sa); + p.diagrams.get_mut(&other_view).unwrap().projection = ProjectionKind::StateMachine { machine }; + p.place(other_view, a_element, point(-100., -50.)); + p.place(other_view, b_element, point(400., -50.)); + let other_machine = p + .add_element(Kind::StateMachine, Layer::Sa, "Other machine", Some(system)) + .unwrap(); + let other_region = p + .add_element(Kind::Region, Layer::Sa, "Other region", Some(other_machine)) + .unwrap(); + let other_state = p + .add_element(Kind::State, Layer::Sa, "Other state", Some(other_region)) + .unwrap(); + let other_diagram = p.ensure_diagram(Layer::Sa); + p.diagrams.get_mut(&other_diagram).unwrap().projection = ProjectionKind::StateMachine { + machine: other_machine, + }; + p.place(other_diagram, other_state, point(25., 50.)); + let opposite = p + .add_element(Kind::Activity, Layer::Oa, "Operational context", None) + .unwrap(); + let architecture = p.ensure_diagram(Layer::Oa); + p.place(architecture, opposite, point(50., 75.)); + validate_candidate(&p).unwrap(); + let mut app = App::with_project(p); + app.page = Page::Behavior; + app.layer = Layer::Sa; + // Deliberately retain a different Architecture diagram. State group edits + // must resolve the live State context, never this legacy field. + app.active_diagram = Some(architecture); + app.behavior.active_owner = Some(machine); + app.behavior.diagram = crate::behavior::state_view::State { + diagram: Some(diagram), + ..Default::default() + }; + app.selected = diagram; + app.selected_node = None; + app.refresh_draft(); + app.reconcile_multiselect(); + assert_eq!(app.state_context().unwrap().diagram, diagram); + Fixture { + app, + diagram, + other_view, + other_machine, + machine, + a, + b, + child, + a_element, + b_element, + internal, + action, + internal_row, + } +} + +fn send(app: &mut App, event: SelectionEvent) { + let _ = app.update(Message::DiagramSelection(event)); +} +fn choose(app: &mut App, target: Target, choice: SelectionChoice) { + let context = app.multiselect_context().unwrap(); + send(app, SelectionEvent::Choose(context, target, choice)); +} +fn pair(f: &mut Fixture) { + choose(&mut f.app, Target::Placement(f.a), SelectionChoice::Replace); + choose(&mut f.app, Target::Placement(f.b), SelectionChoice::Add); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a), Target::Placement(f.b)] + ); + assert_eq!(f.app.behavior.diagram.placement, Some(f.b)); + assert_eq!(f.app.selected_node, None); +} +fn source_matches(app: &App) { + let compiled = source_project::compile_bundle(app.document.bundle(), LIMITS).unwrap(); + assert_eq!(&compiled.decoded.project, app.project()); + assert_eq!(&compiled.bundle, app.document.bundle()); + assert_eq!( + source_workspace::reachable(&app.source_workspace.bundle()).unwrap(), + *app.document.bundle() + ); +} + +#[test] +fn state_fractional_group_move_preserves_full_model_routes_primary_and_one_step_history() { + let mut f = fixture(); + pair(&mut f); + let before = f.app.document.snapshot(); + let selection = f.app.multiselect_snapshot().unwrap(); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.get_mut(&f.a).unwrap().position = point(17.5, 17.); + view.nodes.get_mut(&f.b).unwrap().position = point(517.75, 36.75); + view.nodes.get_mut(&f.child).unwrap().position = point(87.75, 236.75); + view.routes[0].waypoints = vec![point(457.5, 147.)]; + send( + &mut f.app, + SelectionEvent::Move(selection.clone(), point(7.25, -3.5)), + ); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.selected, f.b_element); + assert_eq!(f.app.behavior.diagram.placement, Some(f.b)); + assert_eq!(f.app.selected_node, None); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selection.targets() + ); + source_matches(&f.app); + let after = f.app.document.snapshot(); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + assert_eq!(f.app.behavior.diagram.placement, Some(f.b)); + let _ = f.app.update(Message::Redo); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &after.bundle); + assert!(!f.app.document.can_redo()); + source_matches(&f.app); +} + +#[test] +fn state_selected_ancestor_and_child_move_once_then_hide_only_selected_placements() { + let mut f = fixture(); + choose(&mut f.app, Target::Placement(f.a), SelectionChoice::Replace); + choose(&mut f.app, Target::Placement(f.child), SelectionChoice::Add); + let before = f.app.document.snapshot(); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.get_mut(&f.a).unwrap().position = point(12.75, 24.25); + view.nodes.get_mut(&f.child).unwrap().position = point(83., 244.); + let selection = f.app.multiselect_snapshot().unwrap(); + send( + &mut f.app, + SelectionEvent::Move(selection, point(2.5, 3.75)), + ); + assert_eq!(f.app.project(), &expected); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + pair(&mut f); + let selection = f.app.multiselect_snapshot().unwrap(); + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.remove(&f.a); + view.nodes.remove(&f.b); + send(&mut f.app, SelectionEvent::Hide(selection)); + assert_eq!( + f.app.project(), + &expected, + "Hide preserves the unselected child, routes, semantics and other views" + ); + assert!(f.app.multiselect_snapshot().unwrap().targets().is_empty()); + assert_eq!(f.app.behavior.diagram.placement, None); + assert_eq!(f.app.selected_node, None); + source_matches(&f.app); + let hidden = f.app.document.snapshot(); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + let _ = f.app.update(Message::Redo); + assert_eq!(f.app.project(), &expected); + assert_eq!(f.app.document.bundle(), &hidden.bundle); + assert!(!f.app.document.can_redo()); +} + +#[test] +fn state_rows_are_exact_relation_primaries_and_mixed_move_or_hide_refuses_whole_set() { + for kind in ["action", "internal row", "edge"] { + let mut f = fixture(); + choose(&mut f.app, Target::Placement(f.b), SelectionChoice::Replace); + let target = match kind { + "action" => Target::StateRow { + placement: f.a, + relation: f.action, + }, + "internal row" => Target::StateRow { + placement: f.a, + relation: f.internal_row, + }, + _ => Target::Edge(f.internal), + }; + assert!(f.app.multiselect_target_visible(&target)); + choose(&mut f.app, target.clone(), SelectionChoice::Add); + let selection = f.app.multiselect_snapshot().unwrap(); + assert_eq!( + selection.targets(), + &[Target::Placement(f.b), target.clone()] + ); + assert_eq!(selection.primary(), Some(&target)); + assert_eq!(f.app.behavior.diagram.placement, None); + assert_eq!(f.app.selected_node, None); + let before = f.app.document.snapshot(); + for event in [ + SelectionEvent::Move(selection.clone(), point(7.25, 8.5)), + SelectionEvent::Hide(selection.clone()), + ] { + send(&mut f.app, event); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.multiselect_snapshot().unwrap(), selection); + assert!(!f.app.document.can_undo()); + } + // Even a row selected alone must not use its owner as a placement Hide. + choose(&mut f.app, target, SelectionChoice::Replace); + let row = f.app.multiselect_snapshot().unwrap(); + send(&mut f.app, SelectionEvent::Hide(row)); + assert_eq!(f.app.project(), &before.decoded.project); + assert!(!f.app.document.can_undo()); + } +} + +#[test] +fn state_primary_row_reassignment_or_replacement_preserves_exact_survivor_without_retarget() { + for row_kind in ["action", "internal"] { + for change in ["source", "placement", "collapse"] { + let mut f = fixture(); + let relation = if row_kind == "action" { + f.action + } else { + f.internal_row + }; + let old_row = Target::StateRow { + placement: f.a, + relation, + }; + choose(&mut f.app, Target::Placement(f.b), SelectionChoice::Replace); + choose(&mut f.app, old_row.clone(), SelectionChoice::Add); + let stale = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + let mut candidate = f.app.project().clone(); + match change { + "source" => match &mut candidate.relations.get_mut(&relation).unwrap().body { + RelationBody::StateAction { source, .. } => *source = f.b_element, + RelationBody::StateTransition { source, target, .. } => { + *source = f.b_element; + *target = f.b_element; + } + _ => unreachable!(), + }, + "placement" => { + let view = candidate.diagrams.get_mut(&f.diagram).unwrap(); + let mut replacement = view.nodes.remove(&f.a).unwrap(); + replacement.id = Id::new_v4(); + replacement.alias = "replacement_operating".into(); + view.nodes.insert(replacement.id, replacement); + } + _ => { + candidate + .diagrams + .get_mut(&f.diagram) + .unwrap() + .nodes + .get_mut(&f.a) + .unwrap() + .collapsed = true + } + } + validate_candidate(&candidate).unwrap(); + let _ = f + .app + .update(Message::Command(Command::Replace(candidate.clone()))); + assert_eq!(f.app.project(), &candidate); + assert!(!f.app.multiselect_target_visible(&old_row)); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.b)] + ); + assert_eq!(f.app.selected, f.b_element); + assert_eq!(f.app.behavior.diagram.placement, Some(f.b)); + let live = f.app.multiselect_snapshot().unwrap(); + send(&mut f.app, SelectionEvent::Hide(stale)); + assert_eq!(f.app.project(), &candidate); + assert_eq!(f.app.multiselect_snapshot().unwrap(), live); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.b)] + ); + assert!(!f.app.document.can_undo()); + } + } +} + +#[test] +fn state_primary_placement_removal_or_same_semantic_replacement_preserves_other_member() { + for replace in [false, true] { + let mut f = fixture(); + pair(&mut f); + let mut candidate = f.app.project().clone(); + let view = candidate.diagrams.get_mut(&f.diagram).unwrap(); + let mut old = view.nodes.remove(&f.b).unwrap(); + if replace { + old.id = Id::new_v4(); + old.alias = "replacement_standby".into(); + view.nodes.insert(old.id, old); + } + let _ = f + .app + .update(Message::Command(Command::Replace(candidate.clone()))); + assert_eq!(f.app.project(), &candidate); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a)] + ); + assert_eq!(f.app.selected, f.a_element); + assert_eq!(f.app.behavior.diagram.placement, Some(f.a)); + assert_eq!(f.app.selected_node, None); + let _ = f.app.update(Message::Undo); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + &[Target::Placement(f.a)] + ); + } +} + +#[test] +fn state_stale_group_events_reject_before_status_selection_camera_allocation_or_history_changes() { + for nested in [false, true] { + for stale_by in [ + "document", + "revision", + "view", + "owner", + "tool", + "generation", + ] { + let mut f = fixture(); + pair(&mut f); + let stale = f.app.multiselect_snapshot().unwrap(); + match stale_by { + "document" => f.app.canvas_document = Id::new_v4(), + "revision" => { + f.app + .document + .execute(Command::Describe(f.a_element, "New revision".into())) + .unwrap(); + } + "view" => { + let _ = f.app.update(Message::BehaviorDiagram(super::Choice { + id: f.other_view, + label: "Other saved view".into(), + })); + } + "owner" => { + let _ = f.app.update(Message::BehaviorOwner(super::Choice { + id: f.other_machine, + label: "Other machine".into(), + })); + } + "tool" => { + let _ = f + .app + .update(Message::StateDiagram(StateEvent::Choose(Tool::Transition))); + } + _ => { + choose(&mut f.app, Target::Placement(f.b), SelectionChoice::Toggle); + choose(&mut f.app, Target::Placement(f.b), SelectionChoice::Toggle); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + stale.targets() + ); + } + } + f.app.status = "Current feedback".into(); + f.app.allocation_drag.arrange_next = true; + let before = f.app.document.snapshot(); + let selection = f.app.multiselect_snapshot(); + let selected = ( + f.app.selected, + f.app.selected_node, + f.app.behavior.diagram.placement, + ); + let epochs = ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch, + ); + let camera = ( + f.app.behavior.diagram.zoom, + f.app.behavior.diagram.pan, + f.app.behavior.diagram.fit, + ); + let history = (f.app.document.can_undo(), f.app.document.can_redo()); + let draft = f + .app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); + for event in [ + SelectionEvent::Move(stale.clone(), point(100., 200.)), + SelectionEvent::Hide(stale.clone()), + SelectionEvent::Collapse(stale.clone(), Target::Placement(f.a)), + SelectionEvent::Refuse(stale.clone(), "Stale refusal".into()), + ] { + let message = if nested { + Message::StateDiagram(StateEvent::Selection(event)) + } else { + Message::DiagramSelection(event) + }; + let _ = f.app.update(message); + assert_eq!( + f.app.project(), + &before.decoded.project, + "{stale_by}, nested={nested}" + ); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.multiselect_snapshot(), selection); + assert_eq!( + ( + f.app.selected, + f.app.selected_node, + f.app.behavior.diagram.placement + ), + selected + ); + assert_eq!( + ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch + ), + epochs + ); + assert_eq!( + ( + f.app.behavior.diagram.zoom, + f.app.behavior.diagram.pan, + f.app.behavior.diagram.fit + ), + camera + ); + assert_eq!( + (f.app.document.can_undo(), f.app.document.can_redo()), + history + ); + assert_eq!( + f.app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), + draft + ); + assert!(f.app.allocation_drag.arrange_next); + assert_eq!(f.app.status, "Current feedback"); + } + } + } +} + +#[test] +fn state_pending_property_source_and_state_form_inputs_are_preserved_without_implicit_apply() { + for pending in [ + "valid properties", + "invalid properties", + "source", + "State form", + ] { + let mut f = fixture(); + pair(&mut f); + match pending { + "valid properties" => { + let _ = f + .app + .update(Message::Field("/name".into(), "Retained name".into())); + } + "invalid properties" => { + let _ = f + .app + .update(Message::Field("/alias".into(), "invalid alias!".into())); + } + "source" => { + let text = format!( + "{}\n// Retained raw source\n", + f.app.source_workspace.text() + ); + let _ = f + .app + .update(Message::SourceEdit(text_editor::Action::SelectAll)); + let _ = f.app.update(Message::SourceEdit(text_editor::Action::Edit( + text_editor::Edit::Paste(Arc::new(text)), + ))); + } + _ => { + let _ = f.app.update(Message::Behavior(behavior::Event::States( + states::Event::Edit(f.internal), + ))); + let _ = f.app.update(Message::Behavior(behavior::Event::States( + states::Event::Guard("Retained raw guard".into()), + ))); + } + } + assert!( + f.app.draft_dirty() || f.app.source_dirty() || f.app.behavior.has_pending(), + "{pending}" + ); + let snapshot = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + let source = f.app.source_workspace.bundle(); + let draft = f + .app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()); + let form = f.app.behavior.recovery_snapshot(); + let selected = ( + f.app.selected, + f.app.selected_node, + f.app.behavior.diagram.placement, + ); + for event in [ + SelectionEvent::Move(snapshot.clone(), point(7.25, 9.5)), + SelectionEvent::Hide(snapshot.clone()), + SelectionEvent::Clear(snapshot.context()), + SelectionEvent::Choose( + snapshot.context(), + Target::Placement(f.a), + SelectionChoice::Toggle, + ), + ] { + send(&mut f.app, event); + assert_eq!(f.app.project(), &before.decoded.project, "{pending}"); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.source_workspace.bundle(), source); + assert_eq!( + f.app + .draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()), + draft + ); + assert_eq!(f.app.behavior.recovery_snapshot(), form); + assert_eq!( + ( + f.app.selected, + f.app.selected_node, + f.app.behavior.diagram.placement + ), + selected + ); + assert_eq!(f.app.multiselect_snapshot().unwrap(), snapshot); + assert!(!f.app.document.can_undo()); + } + } +} + +#[test] +fn state_nonselect_tool_denies_current_group_selection_and_commands() { + let mut f = fixture(); + pair(&mut f); + let _ = f + .app + .update(Message::StateDiagram(StateEvent::Choose(Tool::Transition))); + assert_eq!(f.app.behavior.diagram.tool, Tool::Transition); + let current = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + for event in [ + SelectionEvent::Move(current.clone(), point(7.25, 9.5)), + SelectionEvent::Hide(current.clone()), + SelectionEvent::Clear(current.context()), + SelectionEvent::Choose( + current.context(), + Target::Placement(f.a), + SelectionChoice::Toggle, + ), + ] { + send(&mut f.app, event); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.multiselect_snapshot().unwrap(), current); + assert_eq!(f.app.behavior.diagram.tool, Tool::Transition); + assert!(!f.app.document.can_undo()); + } +} + +#[test] +fn state_queued_singleton_move_and_removal_cannot_edit_only_a_member_of_a_group() { + use crate::diagram_usage::{self as usage, Surface}; + let mut f = fixture(); + pair(&mut f); + // Use a real, currently valid navigation marker so late shared hooks cannot + // explain its retirement as cleanup of an invalid sentinel. + f.app.active_diagram = Some(f.diagram); + let marker = usage::collect( + f.app.project(), + f.b_element, + &f.app.behavior.topology, + &f.app.behavior.scenario, + ) + .into_iter() + .find(|u| u.surface == Surface::Saved(f.diagram) && u.target == Target::Placement(f.b)) + .unwrap(); + let reveal_context = usage::Context { + document: f.app.canvas_document, + revision: f.app.document.revision(), + subject: f.b_element, + }; + f.app.pending_diagram_reveal = Some((reveal_context, marker.clone(), f.app.canvas_epoch)); + f.app.diagram_navigation_focus = Some(usage::Focus { + context: reveal_context, + usage: marker, + selected: f.app.selected, + }); + assert!(f.app.diagram_focus(Surface::Saved(f.diagram)).is_some()); + f.app.status = "Current group feedback".into(); + f.app.allocation_drag.arrange_next = true; + let context = f.app.state_context().unwrap(); + let selection = f.app.multiselect_snapshot().unwrap(); + let before = f.app.document.snapshot(); + let pending = f.app.pending_diagram_reveal.clone(); + let focus = f + .app + .diagram_navigation_focus + .as_ref() + .map(|f| (f.context, f.usage.clone(), f.selected)); + let epochs = ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch, + ); + let camera = ( + f.app.behavior.diagram.zoom, + f.app.behavior.diagram.pan, + f.app.behavior.diagram.fit, + ); + let selected = ( + f.app.selected, + f.app.selected_node, + f.app.behavior.diagram.placement, + ); + // These events deliberately have the live document/view/epoch context. + // Group admission must reject them before navigation and allocation hooks. + for message in [ + Message::StateDiagram(StateEvent::Select(context, f.a)), + Message::StateDiagram(StateEvent::Move { + context, + node: f.b, + position: point(700., 500.), + }), + Message::StateDiagram(StateEvent::Remove(diagram_removal::Intent { + context, + object: f.b_element, + placement: Some(f.b), + model: false, + })), + ] { + let _ = f.app.update(message); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.multiselect_snapshot().unwrap(), selection); + assert_eq!(f.app.pending_diagram_reveal, pending); + assert_eq!( + f.app.diagram_navigation_focus.as_ref().map(|f| ( + f.context, + f.usage.clone(), + f.selected + )), + focus + ); + assert_eq!( + ( + f.app.canvas_epoch, + f.app.camera_epoch, + f.app.allocation_drag.epoch + ), + epochs + ); + assert_eq!( + ( + f.app.behavior.diagram.zoom, + f.app.behavior.diagram.pan, + f.app.behavior.diagram.fit + ), + camera + ); + assert_eq!( + ( + f.app.selected, + f.app.selected_node, + f.app.behavior.diagram.placement + ), + selected + ); + assert_eq!(f.app.status, "Current group feedback"); + assert!(f.app.allocation_drag.arrange_next); + assert!(!f.app.delete_pending); + assert!(!f.app.document.can_undo()); + assert!(!f.app.document.can_redo()); + } + // Retain the existing model-level checks for generic singleton removal. + for message in [ + Message::DiagramRemove(diagram_removal::Intent { + context, + object: f.b_element, + placement: Some(f.b), + model: true, + }), + Message::Delete, + ] { + let _ = f.app.update(message); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selection.targets() + ); + assert!(!f.app.delete_pending); + assert!(!f.app.document.can_undo()); + } +} + +#[test] +fn state_boundary_entry_exit_group_moves_use_authored_positions_and_boundary_owner_pin_rules() { + for kind in [Kind::EntryPoint, Kind::ExitPoint] { + for boundary in ["fixed", "moved", "omitted"] { + let mut f = fixture(); + let mut p = f.app.project().clone(); + let child_element = p.diagrams[&f.diagram].nodes[&f.child].element; + let region = p.elements[&child_element].parent.unwrap(); + let symbol = p + .add_element(kind, Layer::Sa, "Boundary symbol", Some(region)) + .unwrap(); + let edge = if kind == Kind::EntryPoint { + p.relate( + RelationKind::StateTransition, + "Enter", + symbol, + child_element, + ) + } else { + p.relate(RelationKind::StateTransition, "Exit", child_element, symbol) + }; + let placement = p.place(f.diagram, symbol, point(4000.25, 300.5)); + p.diagrams + .get_mut(&f.diagram) + .unwrap() + .routes + .push(route(edge, vec![point(140.25, 320.5)])); + // Keep the authored symbol inside the existing boundary so the + // projection does not independently grow that boundary to enclose it. + p.diagrams + .get_mut(&f.diagram) + .unwrap() + .nodes + .get_mut(&f.a) + .unwrap() + .size + .width = 5000.; + if boundary == "omitted" { + p.diagrams.get_mut(&f.diagram).unwrap().nodes.remove(&f.a); + } + validate_candidate(&p).unwrap(); + let architecture = f.app.active_diagram; + f.app = App::with_project(p); + f.app.page = Page::Behavior; + f.app.layer = Layer::Sa; + f.app.active_diagram = architecture; + f.app.behavior.active_owner = Some(f.machine); + f.app.behavior.diagram = crate::behavior::state_view::State { + diagram: Some(f.diagram), + ..Default::default() + }; + f.app.selected = f.diagram; + f.app.refresh_draft(); + f.app.reconcile_multiselect(); + choose( + &mut f.app, + Target::Placement(placement), + SelectionChoice::Replace, + ); + choose(&mut f.app, Target::Placement(f.child), SelectionChoice::Add); + if boundary == "moved" { + choose(&mut f.app, Target::Placement(f.a), SelectionChoice::Add); + } + let before = f.app.document.snapshot(); + let projected = projection::project_view(f.app.project(), f.diagram); + let old_symbol = projected + .nodes + .iter() + .find(|n| n.id == placement) + .unwrap() + .position; + if boundary != "omitted" { + assert_ne!( + old_symbol.x, 4000.25, + "authored anchor differs from projected boundary" + ); + } + let mut expected = f.app.project().clone(); + let view = expected.diagrams.get_mut(&f.diagram).unwrap(); + view.nodes.get_mut(&placement).unwrap().position = point(4007.5, 305.25); + view.nodes.get_mut(&f.child).unwrap().position = point(87.75, 245.); + if boundary == "moved" { + view.nodes.get_mut(&f.a).unwrap().position = point(17.5, 25.25); + } + if boundary != "fixed" { + view.routes.last_mut().unwrap().waypoints = vec![point(147.5, 325.25)]; + } + let snapshot = f.app.multiselect_snapshot().unwrap(); + send( + &mut f.app, + SelectionEvent::Move(snapshot, point(7.25, 4.75)), + ); + assert_eq!(f.app.project(), &expected, "{kind:?}/{boundary}"); + let after = projection::project_view(f.app.project(), f.diagram); + let new_symbol = after + .nodes + .iter() + .find(|n| n.id == placement) + .unwrap() + .position; + assert_eq!( + new_symbol.x, + old_symbol.x + if boundary == "fixed" { 0. } else { 7.25 } + ); + source_matches(&f.app); + let _ = f.app.update(Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + } + } +} + +#[tokio::test] +async fn state_actions_trigger_captures_exact_row_without_owner_placement_and_stale_menu_cannot_retarget() + { + use crate::{ + context_menu::Entry, + model_menu::{self, Event as MenuEvent}, + }; + use iced::advanced::renderer::Headless; + use iced::{Event, Point, Size, mouse}; + use iced_runtime::user_interface::{Cache, UserInterface}; + let mut renderer = ::new(iced::Font::DEFAULT, 16.into(), None) + .await + .expect("State Actions trigger requires the supported headless renderer"); + for internal in [false, true] { + let mut f = fixture(); + let relation = if internal { f.internal_row } else { f.action }; + let row = Target::StateRow { + placement: f.a, + relation, + }; + choose(&mut f.app, Target::Placement(f.b), SelectionChoice::Replace); + choose(&mut f.app, row.clone(), SelectionChoice::Add); + let before = f.app.document.snapshot(); + let selection = f.app.multiselect_snapshot().unwrap(); + // Exercise the real Actions button closure, not a reconstruction of its + // Request: the typed row must survive without the owner's placement. + let mut messages = vec![]; + { + let mut ui = UserInterface::build( + model_menu::trigger(&f.app), + Size::new(800., 100.), + Cache::new(), + &mut renderer, + ); + let _ = ui.update( + &[Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + ))], + mouse::Cursor::Available(Point::new(5., 5.)), + &mut renderer, + &mut iced::advanced::clipboard::Null, + &mut messages, + ); + } + assert_eq!(messages.len(), 1); + let Message::Menu(MenuEvent::Open(request)) = messages.remove(0) else { + panic!("Actions opens a menu") + }; + assert_eq!(request.object, relation); + assert_eq!(request.placement, None); + assert_eq!(request.selection_target.as_deref(), Some(&row)); + assert_eq!(request.canvas, f.app.state_context()); + let mut wrong = request.clone(); + wrong.placement = Some(f.a); + assert!(model_menu::Opened::new(&f.app, wrong, 1).is_none()); + let mut wrong = request.clone(); + wrong.object = f.a_element; + assert!(model_menu::Opened::new(&f.app, wrong, 1).is_none()); + let _ = f + .app + .update(Message::Menu(MenuEvent::Open(request.clone()))); + let opened = f.app.model_menu.as_ref().unwrap(); + assert_eq!(opened.request.selection_target.as_deref(), Some(&row)); + assert_eq!(opened.request.placement, None); + assert_eq!( + f.app.multiselect_snapshot().unwrap().targets(), + selection.targets() + ); + assert_eq!(f.app.multiselect_snapshot().unwrap().primary(), Some(&row)); + assert_eq!(f.app.behavior.diagram.placement, None); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + let entries = model_menu::popup(&f.app).unwrap().entries; + assert!(entries.iter().any(|entry| matches!(entry, Entry::Action { label, message: None, .. } if label == "Hide selected placements"))); + let invoke = entries + .into_iter() + .find_map(|entry| match entry { + Entry::Action { label, message, .. } if label == "Select only this element" => { + message + } + _ => None, + }) + .expect("row group menu exposes exact selection command"); + // Keep the captured menu object alive across a model revision, so this + // checks its validation instead of relying on proactive menu dismissal. + let mut candidate = f.app.project().clone(); + match &mut candidate.relations.get_mut(&relation).unwrap().body { + RelationBody::StateAction { source, .. } => *source = f.b_element, + RelationBody::StateTransition { source, target, .. } => { + *source = f.b_element; + *target = f.b_element; + } + _ => unreachable!(), + } + f.app + .document + .execute(Command::Replace(candidate.clone())) + .unwrap(); + assert!(!f.app.model_menu.as_ref().unwrap().valid(&f.app)); + let source = f.app.document.bundle().clone(); + let current = f.app.multiselect_snapshot().unwrap(); + let _ = f.app.update(invoke); + assert_eq!(f.app.project(), &candidate); + assert_eq!(f.app.document.bundle(), &source); + assert_eq!(f.app.multiselect_snapshot().unwrap(), current); + assert_eq!(f.app.selected, relation); + assert_eq!(f.app.behavior.diagram.placement, None); + assert!(f.app.model_menu.is_none()); + let mut stale_row = request; + stale_row.canvas = f.app.state_context(); + assert!( + model_menu::Opened::new(&f.app, stale_row.clone(), 2).is_none(), + "a fresh envelope cannot rebind the old row occurrence" + ); + stale_row.selection_target = Some(Box::new(Target::StateRow { + placement: f.b, + relation, + })); + assert!( + model_menu::Opened::new(&f.app, stale_row, 2).is_some(), + "the replacement occurrence is independently visible" + ); + } +} diff --git a/crates/oa-desktop/src/behavior/state_bend_tests.rs b/crates/oa-desktop/src/behavior/state_bend_tests.rs index f4635ab9..cac001f0 100644 --- a/crates/oa-desktop/src/behavior/state_bend_tests.rs +++ b/crates/oa-desktop/src/behavior/state_bend_tests.rs @@ -391,7 +391,7 @@ fn state_bends_context_menu_distinguishes_authored_pin_from_computed_segment() { motion(&canvas, &mut state, pin, pin); assert!( matches!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Right), pin), - Some(Event::ContextMenu { context, object, placement: None, anchor, bend: Some(0), route_segment: None }) + Some(Event::ContextMenu { context, object, placement: None, anchor, bend: Some(0), route_segment: None, .. }) if context == canvas.context && object == relation && anchor == pin) ); let (index, midpoint) = bend_segment(&scene, relation); @@ -399,7 +399,7 @@ fn state_bends_context_menu_distinguishes_authored_pin_from_computed_segment() { motion(&canvas, &mut state, at, at); assert!( matches!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Right), at), - Some(Event::ContextMenu { context, object, placement: None, anchor, bend: None, route_segment: Some((segment, world)) }) + Some(Event::ContextMenu { context, object, placement: None, anchor, bend: None, route_segment: Some((segment, world)), .. }) if context == canvas.context && object == relation && anchor == at && segment == index && point(world).distance(midpoint) < 0.001) ); } diff --git a/crates/oa-desktop/src/behavior/state_canvas.rs b/crates/oa-desktop/src/behavior/state_canvas.rs index 2893b6aa..8b6d2a74 100644 --- a/crates/oa-desktop/src/behavior/state_canvas.rs +++ b/crates/oa-desktop/src/behavior/state_canvas.rs @@ -1,5 +1,11 @@ //! Persisted state-diagram geometry with context-bound, ordered pointer gestures. use super::state_edit::{self, Context, Endpoint, Event, Tool}; +use crate::app::multiselect::{Choice, Event as SelectionEvent, context as selection_context}; +use crate::diagram::group_gesture::{ + DisplacementPolicy, Gesture as GroupGesture, Outcome, Viewport, +}; +use crate::diagram_selection::{Selection, Snapshot}; +use crate::diagram_usage::Target; use crate::style; use iced::{ Color, Point, Rectangle, Renderer, Size, Theme, Vector, keyboard, mouse, @@ -27,6 +33,7 @@ pub struct StateCanvas<'a> { pub diagram: Id, pub context: Context, pub selected: Id, + pub selection: Option<&'a Selection>, /// Presentation-only navigation marker; never an authoring placement. pub navigation_focus: Option, pub tool: Tool, @@ -41,6 +48,8 @@ pub struct State { tool: Option, pointer: Option, press: Option, + group: Option, + modifiers: keyboard::Modifiers, source: Option, preview: Option<(Id, Position)>, attachment_preview: Option, @@ -113,6 +122,7 @@ struct Scene { } struct CachedScene { context: Context, + group_preview: Option<(Snapshot, Position)>, preview: Option<(Id, Position)>, attachment_preview: Option, bend_preview: Option, @@ -121,6 +131,8 @@ struct CachedScene { #[derive(PartialEq)] struct PaintKey { context: Context, + group_preview: Option<(Snapshot, Position)>, + selection: Option, preview: Option<(Id, Position)>, attachment_preview: Option, bend_preview: Option, @@ -132,12 +144,82 @@ struct PaintKey { enum Hit { Node { placement: Id, element: Id }, Transition(Id), - StateAction(Id), + Row { placement: Id, relation: Id }, +} + +impl Hit { + fn target(self) -> Target { + match self { + Self::Node { placement, .. } => Target::Placement(placement), + Self::Transition(id) => Target::Edge(id), + Self::Row { + placement, + relation, + } => Target::StateRow { + placement, + relation, + }, + } + } +} + +/// Unsupported mixtures retain the same click/cancel lifecycle as placement groups. +struct GroupPress { + snapshot: Snapshot, + target: Target, + start: Point, + viewport: Viewport, + movement: Option, + crossed: bool, + retired: bool, +} + +impl GroupPress { + fn move_to(&mut self, pointer: Point) { + if self.retired { + return; + } + if !pointer.x.is_finite() + || !pointer.y.is_finite() + || !self.viewport.bounds.contains(pointer) + { + self.retired = true; + return; + } + self.crossed |= pointer.distance(self.start) > 3.; + if let Some(movement) = &mut self.movement + && !movement.move_to(pointer) + { + self.retired = true; + } + } + + fn release(mut self, pointer: Point) -> Option { + self.move_to(pointer); + if self.retired { + return None; + } + if !self.crossed { + return Some(SelectionEvent::Collapse(self.snapshot, self.target)); + } + let Some(movement) = self.movement else { + return Some(SelectionEvent::Refuse(self.snapshot, + "Move requires only placed elements. Remove rows, ports and transitions from the selection.".into())); + }; + match movement.release(pointer)? { + Outcome::Click(id) => Some(SelectionEvent::Collapse( + self.snapshot, + Target::Placement(id), + )), + Outcome::Move(snapshot, delta) => Some(SelectionEvent::Move(snapshot, delta)), + } + } } impl State { fn cancel_edit(&mut self) { self.press = None; + self.group = None; self.preview = None; self.attachment_preview = None; self.bend = None; @@ -485,6 +567,32 @@ pub(crate) fn visible_row_relation( }) } +/// Validate this occurrence, including the relation's current source. A surviving +/// relation reassigned to another State must not rebound an old row selection. +pub(crate) fn visible_row_target( + project: &Project, + diagram: Id, + view: &ProjectedView, + placement: Id, + relation: Id, +) -> bool { + visible_row_relation(project, diagram, view, relation) + && view + .nodes + .iter() + .find(|node| node.id == placement) + .is_some_and(|node| { + project + .relations + .get(&relation) + .is_some_and(|row| row.source() == node.element) + && state_action_rows(project, node) + .into_iter() + .chain(internal_rows(project, node)) + .any(|(row, _)| row.id == relation) + }) +} + fn state_action_label(project: &Project, relation: &Relation) -> String { let RelationBody::StateAction { target, phase, .. } = relation.body else { return String::new(); @@ -663,6 +771,210 @@ pub(crate) fn diagram_usages( } impl StateCanvas<'_> { + fn current_selection(&self) -> Option<&Selection> { + self.selection + .filter(|s| s.scope() == selection_context(self.context).scope) + } + + fn group_viewport(&self, bounds: Rectangle) -> Viewport { + Viewport { + zoom: self.scale(), + pan: self.pan, + bounds: Rectangle::new( + Point::new(bounds.x, bounds.y + GUIDANCE_HEIGHT), + Self::content(bounds).size(), + ), + } + } + + fn group_current(&self, group: &GroupPress, bounds: Option) -> bool { + !group.retired + && self.editable + && self.tool == Tool::Select + && group.viewport.zoom == self.scale() + && group.viewport.pan == self.pan + && bounds.is_none_or(|b| group.viewport == self.group_viewport(b)) + && self + .current_selection() + .is_some_and(|s| s.matches(&group.snapshot, selection_context(self.context))) + } + + fn group_preview_key( + &self, + state: &State, + bounds: Option, + ) -> Option<(Snapshot, Position)> { + let group = state.group.as_ref()?; + if !self.group_current(group, bounds) || !group.crossed { + return None; + } + Some((group.snapshot.clone(), group.movement.as_ref()?.delta())) + } + + fn group_input( + &self, + state: &mut State, + event: &canvas::Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + if let Some(group) = &state.group { + let cancelled = matches!( + event, + canvas::Event::Mouse( + mouse::Event::CursorLeft + | mouse::Event::WheelScrolled { .. } + | mouse::Event::ButtonPressed(mouse::Button::Right | mouse::Button::Middle) + ) | canvas::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(keyboard::key::Named::Escape), + .. + }) + ); + if !self.group_current(group, Some(bounds)) || cancelled { + state.cancel_edit(); + return Some(Action::request_redraw().and_capture()); + } + } + if let Some(group) = &mut state.group { + match event { + canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => { + group.move_to(*position); + return Some(Action::request_redraw().and_capture()); + } + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let group = state.group.take().unwrap(); + return Some( + cursor + .position() + .and_then(|p| group.release(p)) + .map_or_else( + || Action::request_redraw().and_capture(), + |event| Action::publish(Event::Selection(event)).and_capture(), + ), + ); + } + canvas::Event::Keyboard(_) + | canvas::Event::Mouse(mouse::Event::ButtonPressed(_)) => { + return Some(Action::capture()); + } + _ => {} + } + } + let selection = self.current_selection()?; + if !self.editable || self.tool != Tool::Select || state.pan_from.is_some() { + return None; + } + if let canvas::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event { + use keyboard::{Key, key::Named}; + if *key == Key::Named(Named::Escape) && state.press.is_none() && state.source.is_none() + { + return Some( + Action::publish(Event::Selection(SelectionEvent::Clear(selection_context( + self.context, + )))) + .and_capture(), + ); + } + if selection.targets().len() > 1 && *key == Key::Named(Named::Delete) { + let snapshot = selection.snapshot(selection_context(self.context))?; + let event = if modifiers.is_empty() + && crate::app::multiselect::placements(&snapshot).is_some() + { + SelectionEvent::Hide(snapshot) + } else { + SelectionEvent::Refuse(snapshot, if modifiers.is_empty() { + "Hide requires only placed elements. Rows, ports and transitions cannot be hidden as a placement group." + } else { + "Group model deletion requires an aggregate impact review and is not available yet. Use Hide selected for placements." + }.into()) + }; + return Some(Action::publish(Event::Selection(event)).and_capture()); + } + } + if !matches!( + event, + canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) { + return None; + } + let window = cursor + .position() + .filter(|p| self.group_viewport(bounds).bounds.contains(*p))?; + let local = Point::new(window.x - bounds.x, window.y - bounds.y); + let scene = self.scene(state)?; + let world = self.world(local); + // Route handles belong to the exact edge, including when an endpoint + // overlaps a node. They cannot bypass Shift/group admission. + let handle = self + .endpoint_at(&scene, world) + .map(|(id, _)| Target::Edge(id)) + .or_else(|| { + self.bend_at(&scene, world) + .map(|_| Target::Edge(self.selected)) + }); + let target = handle.or_else(|| self.hit(&scene, world).map(Hit::target))?; + if state.modifiers.shift() { + state.cancel_edit(); + return Some( + Action::publish(Event::Selection(SelectionEvent::Choose( + selection_context(self.context), + target, + Choice::Toggle, + ))) + .and_capture(), + ); + } + if selection.targets().len() <= 1 || !selection.contains(&target) { + return None; + } + let snapshot = selection.snapshot(selection_context(self.context))?; + let viewport = self.group_viewport(bounds); + let movement = if crate::app::multiselect::placements(&snapshot).is_some() + && let Target::Placement(anchor) = target + { + let original = self + .project + .diagrams + .get(&self.diagram)? + .nodes + .get(&anchor)? + .position; + Some(GroupGesture::new_with_policy( + snapshot.clone(), + anchor, + original, + window, + viewport, + DisplacementPolicy::Unsnapped, + )?) + } else { + None + }; + state.cancel_edit(); + state.group = Some(GroupPress { + snapshot, + target, + start: window, + viewport, + movement, + crossed: false, + retired: false, + }); + Some(Action::request_redraw().and_capture()) + } + + fn select_event(&self, hit: Hit, semantic: Id) -> Event { + if self.current_selection().is_some() { + Event::Selection(SelectionEvent::Choose( + selection_context(self.context), + hit.target(), + Choice::Replace, + )) + } else { + Event::Select(self.context, semantic) + } + } + fn scale(&self) -> f32 { if self.zoom.is_finite() { self.zoom.clamp(0.0001, 3.) @@ -695,6 +1007,7 @@ impl StateCanvas<'_> { return None; } let route_active = self.gesture_active(state) && self.route_viewport_current(state, bounds); + let group_preview = self.group_preview_key(state, bounds); let preview = self .gesture_active(state) .then_some(state.preview) @@ -703,6 +1016,7 @@ impl StateCanvas<'_> { let bend_preview = route_active.then(|| state.bend_preview.clone()).flatten(); if let Some(cached) = state.scene.borrow().as_ref() && cached.context == self.context + && cached.group_preview == group_preview && cached.preview == preview && cached.attachment_preview == attachment_preview && cached.bend_preview == bend_preview @@ -710,6 +1024,27 @@ impl StateCanvas<'_> { return Some(cached.scene.clone()); } let mut diagram = self.project.diagrams.get(&self.diagram)?.clone(); + if let Some((snapshot, delta)) = &group_preview + && let Some(ids) = crate::app::multiselect::placements(snapshot) + && let Ok(Command::Batch(commands)) = oa_core::diagram_edit::translate_placements( + self.project, + self.diagram, + &ids, + *delta, + ) + { + for command in commands { + match command { + Command::EditNode { node, .. } => { + diagram.nodes.insert(node.id, node); + } + Command::SetEdgeRoute { route, .. } => { + crate::route_edit::with_route(&mut diagram, route) + } + _ => unreachable!("placement planner returned a non-geometry command"), + } + } + } if let Some(route) = &bend_preview { crate::route_edit::with_route(&mut diagram, route.clone()); } @@ -761,6 +1096,7 @@ impl StateCanvas<'_> { }); *state.scene.borrow_mut() = Some(CachedScene { context: self.context, + group_preview, preview, attachment_preview, bend_preview, @@ -823,13 +1159,19 @@ impl StateCanvas<'_> { .into_iter() .find(|(_, r)| r.contains(world)) { - return Some(Hit::StateAction(relation.id)); + return Some(Hit::Row { + placement: node.id, + relation: relation.id, + }); } if let Some((relation, _)) = internal_rows(self.project, node) .into_iter() .find(|(_, r)| r.contains(world)) { - return Some(Hit::Transition(relation.id)); + return Some(Hit::Row { + placement: node.id, + relation: relation.id, + }); } let container = self .project @@ -1148,7 +1490,13 @@ impl StateCanvas<'_> { }, _ => match world.and_then(|p| self.hit(scene, p)) { Some(Hit::Transition(id)) => self.project.relations.get(&id).map(|r| format!("{} · {} · right-click to edit", r.name, routing::state_transition_summary(self.project, r))).unwrap_or_default(), - Some(Hit::StateAction(id)) => self.project.relations.get(&id).map(|r| format!("{} · select the action link to inspect its referenced work", state_action_label(self.project, r))).unwrap_or_default(), + Some(Hit::Row { relation: id, .. }) => self.project.relations.get(&id).map(|r| { + if matches!(r.body, RelationBody::StateAction { .. }) { + format!("{} · select the action link to inspect its referenced work", state_action_label(self.project, r)) + } else { + format!("{} · {} · right-click to edit", r.name, routing::state_transition_summary(self.project, r)) + } + }).unwrap_or_default(), _ => "Select / move · drag a vertex · drag selected transition endpoints to reconnect · middle-drag to pan".into(), }, } @@ -1172,6 +1520,15 @@ impl canvas::Program for StateCanvas<'_> { bounds: Rectangle, cursor: mouse::Cursor, ) -> Option> { + if let canvas::Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event { + // Passive observation must not capture the event or grant focus. + state.modifiers = *modifiers; + return None; + } + let retired_group = state.group.is_some() + && (state.context != Some(self.context) + || state.tool != Some(self.tool) + || !self.editable); if state.context != Some(self.context) || state.tool != Some(self.tool) { if state.context != Some(self.context) { state.viewport = None; @@ -1226,6 +1583,7 @@ impl canvas::Program for StateCanvas<'_> { cursor } canvas::Event::Window(iced::window::Event::Unfocused) => { + state.modifiers = keyboard::Modifiers::default(); state.reset(); return None; } @@ -1239,6 +1597,14 @@ impl canvas::Program for StateCanvas<'_> { let window = cursor.position(); let local = window.map(|p| Point::new(p.x - bounds.x, p.y - bounds.y)); let inside = local.is_some_and(|p| Self::content(bounds).contains(p)); + if retired_group { + // The event which retired a held gesture cannot be reinterpreted as + // a fresh keyboard edit against the surviving selection. + return Some(Action::request_redraw().and_capture()); + } + if let Some(action) = self.group_input(state, event, bounds, cursor) { + return Some(action); + } if let canvas::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event { use keyboard::{Key, key::Named}; if *key == Key::Named(Named::Escape) { @@ -1310,13 +1676,17 @@ impl canvas::Program for StateCanvas<'_> { // Menu commands must target the committed route instead. let scene = self.scene(state)?; let bend = self.bend_at(&scene, world); - let (object, placement) = if bend.is_some() { - (self.selected, None) + let (object, placement, selection_target) = if bend.is_some() { + (self.selected, None, Some(Target::Edge(self.selected))) } else { match self.hit(&scene, world) { - Some(Hit::Node { element, placement }) => (element, Some(placement)), - Some(Hit::Transition(id) | Hit::StateAction(id)) => (id, None), - None => (self.diagram, None), + Some(hit @ Hit::Node { element, placement }) => { + (element, Some(placement), Some(hit.target())) + } + Some(hit @ (Hit::Transition(id) | Hit::Row { relation: id, .. })) => { + (id, None, Some(hit.target())) + } + None => (self.diagram, None, None), } }; let route_segment = if bend.is_none() { @@ -1329,6 +1699,7 @@ impl canvas::Program for StateCanvas<'_> { context: self.context, object, placement, + selection_target, anchor: window, bend, route_segment, @@ -1336,12 +1707,17 @@ impl canvas::Program for StateCanvas<'_> { .and_capture(), ); } - if *button != mouse::Button::Left || !self.editable { + if *button != mouse::Button::Left || !self.editable || state.pan_from.is_some() { return None; } match self.tool { Tool::Select => { - if let Some((relation, endpoint)) = self.endpoint_at(&scene, world) { + let singleton = self + .current_selection() + .is_none_or(|s| s.targets().len() <= 1); + if singleton + && let Some((relation, endpoint)) = self.endpoint_at(&scene, world) + { state.press = Some(Press::Reconnect { relation, endpoint, @@ -1349,7 +1725,7 @@ impl canvas::Program for StateCanvas<'_> { }); return Some(Action::capture()); } - if let Some(index) = self.bend_at(&scene, world) { + if singleton && let Some(index) = self.bend_at(&scene, world) { let route = crate::route_edit::stored_route( &self.project.diagrams[&self.diagram], self.selected, @@ -1358,7 +1734,7 @@ impl canvas::Program for StateCanvas<'_> { return Some(Action::capture()); } match self.hit(&scene, world) { - Some(Hit::Node { placement, element }) => { + Some(hit @ Hit::Node { placement, element }) => { let original = self .project .diagrams @@ -1371,13 +1747,11 @@ impl canvas::Program for StateCanvas<'_> { start: window, original, }); - Some( - Action::publish(Event::Select(self.context, element)) - .and_capture(), - ) + Some(Action::publish(self.select_event(hit, element)).and_capture()) } - Some(Hit::Transition(id) | Hit::StateAction(id)) => { - if id == self.selected + Some(hit @ (Hit::Transition(id) | Hit::Row { relation: id, .. })) => { + if singleton + && id == self.selected && let Some(edge) = scene.routed.edges.get(&id) && !Rectangle::new( point(edge.label.bounds.position), @@ -1407,7 +1781,7 @@ impl canvas::Program for StateCanvas<'_> { return Some(Action::capture()); } } - Some(Action::publish(Event::Select(self.context, id)).and_capture()) + Some(Action::publish(self.select_event(hit, id)).and_capture()) } None => { state.pan_from = Some((mouse::Button::Left, window)); @@ -1643,6 +2017,10 @@ impl canvas::Program for StateCanvas<'_> { self.gesture_active(state) && self.route_viewport_current(state, Some(bounds)); let key = PaintKey { context: self.context, + group_preview: self.group_preview_key(state, Some(bounds)), + selection: self + .current_selection() + .and_then(|s| s.snapshot(selection_context(self.context))), preview: self .gesture_active(state) .then_some(state.preview) @@ -1671,23 +2049,40 @@ impl canvas::Program for StateCanvas<'_> { frame.translate(self.offset()); frame.scale(self.scale()); for node in &scene.view.nodes { + let selected = + self.current_selection() + .map_or(self.selected, |s| match s.primary() { + Some(Target::Placement(id)) if *id == node.id => node.element, + Some(Target::StateRow { + placement, + relation, + }) if *placement == node.id => *relation, + _ => Id::nil(), + }); paint_node( frame, self.project, node, - self.selected, + selected, scene.symbol_labels.get(&node.id), ); } // Containers are already painted. Route bodies have shared // clearance from state headers and internal-transition rows. for edge in scene.routed.edges.values() { - let active = edge.id == self.selected; + let active = self + .current_selection() + .map_or(edge.id == self.selected, |s| { + s.contains(&Target::Edge(edge.id)) + }); + let primary = self + .current_selection() + .map_or(active, |s| s.primary() == Some(&Target::Edge(edge.id))); let color = if active { style::TEAL } else { style::INK }; frame.stroke( &polyline(&edge.points), Stroke::default().with_color(color).with_width(if active { - 2.4 + if primary { 3.2 } else { 2.0 } } else { 1.4 }), @@ -1727,7 +2122,11 @@ impl canvas::Program for StateCanvas<'_> { 10., style::MUTED, ); - if active { + if active + && self + .current_selection() + .is_none_or(|s| s.targets().len() <= 1) + { if !edge.custom_suspended && self.tool == Tool::Select { for pin in &edge.pins { let radius = 4. / self.scale(); @@ -1758,6 +2157,42 @@ impl canvas::Program for StateCanvas<'_> { } } } + if let Some(selection) = self.current_selection() { + for target in selection.targets() { + let rect = match target { + Target::Placement(id) => { + scene.view.nodes.iter().find(|n| n.id == *id).map(rectangle) + } + Target::StateRow { + placement, + relation, + } => scene + .view + .nodes + .iter() + .find(|n| n.id == *placement) + .and_then(|node| { + state_action_rows(self.project, node) + .into_iter() + .chain(internal_rows(self.project, node)) + .find(|(row, _)| row.id == *relation) + .map(|(_, rect)| rect) + }), + _ => None, + }; + if let Some(rect) = rect { + let primary = selection.primary() == Some(target); + let rect = + expanded(rect, if primary { 3. } else { 1.5 } / self.scale()); + frame.stroke( + &Path::rectangle(rect.position(), rect.size()), + Stroke::default() + .with_color(style::TEAL) + .with_width(if primary { 3. } else { 1.5 } / self.scale()), + ); + } + } + } }); }); let mut overlay = Frame::new(renderer, bounds.size()); @@ -2159,6 +2594,7 @@ mod tests { use crate::document::Document; include!("state_bend_tests.rs"); + include!("state_multiselect_canvas_tests.rs"); struct Fixture { project: Project, @@ -2313,7 +2749,10 @@ mod tests { let scene = canvas.scene(&State::default()).unwrap(); assert_eq!( canvas.hit(&scene, row.bounds.center()), - Some(Hit::StateAction(action)) + Some(Hit::Row { + placement, + relation: action + }) ); assert_eq!(f.project, before); f.project @@ -2428,6 +2867,7 @@ mod tests { epoch: 3, }, selected: f.machine, + selection: None, navigation_focus: None, tool, zoom, @@ -3246,7 +3686,10 @@ mod tests { for (relation, row) in rows { assert_eq!( canvas.hit(&scene, row.center()), - Some(Hit::StateAction(relation.id)) + Some(Hit::Row { + placement: a.id, + relation: relation.id + }) ); let events = click(&canvas, &mut State::default(), row.center()); assert!( @@ -3307,7 +3750,10 @@ mod tests { for (relation, rect) in rows { assert_eq!( canvas.hit(&scene, rect.center()), - Some(Hit::Transition(relation.id)) + Some(Hit::Row { + placement: a.id, + relation: relation.id + }) ); } let initial = scene diff --git a/crates/oa-desktop/src/behavior/state_edit.rs b/crates/oa-desktop/src/behavior/state_edit.rs index 6cb876e1..6a650ed5 100644 --- a/crates/oa-desktop/src/behavior/state_edit.rs +++ b/crates/oa-desktop/src/behavior/state_edit.rs @@ -25,6 +25,7 @@ pub enum Tool { #[derive(Debug, Clone)] pub enum Event { + Selection(crate::app::multiselect::Event), Remove(crate::app::diagram_removal::Intent), Choose(Tool), CreateView(Id), @@ -38,6 +39,7 @@ pub enum Event { context: Context, object: Id, placement: Option, + selection_target: Option, anchor: iced::Point, bend: Option, route_segment: Option<(usize, Position)>, @@ -101,7 +103,8 @@ impl Event { pub fn context(&self) -> Option { match self { Self::Remove(intent) => Some(intent.context), - Self::Choose(_) | Self::CreateView(_) => None, + // Group events carry their own exact selection envelope. + Self::Selection(_) | Self::Choose(_) | Self::CreateView(_) => None, Self::Select(c, _) | Self::Pan(c, _) | Self::Zoom(c, _) diff --git a/crates/oa-desktop/src/behavior/state_multiselect_canvas_tests.rs b/crates/oa-desktop/src/behavior/state_multiselect_canvas_tests.rs new file mode 100644 index 00000000..48860f5a --- /dev/null +++ b/crates/oa-desktop/src/behavior/state_multiselect_canvas_tests.rs @@ -0,0 +1,395 @@ +// Included in state_canvas::tests. Drive production Program input and projection. + +#[test] +fn state_singleton_selection_bend_uses_five_unit_grid_and_escape_keeps_selection() { + for zoom in [0.5, 1., 2.] { + for cancel in [false, true] { + let (f, relation) = bend_fixture(true); + let original = f.project.diagrams[&f.diagram].routes[0].clone(); + let mut canvas = canvas(&f, Tool::Select, zoom); + canvas.selected = relation; + let selection = group_selection(&canvas, [Target::Edge(relation)]); + let snapshot = selection.snapshot(selection_context(canvas.context)).unwrap(); + canvas.selection = Some(&selection); + let mut state = State::default(); + let baseline = canvas.scene(&state).unwrap(); + let start = screen(&canvas, point(original.waypoints[0])); + // A fractional pointer target distinguishes route snapping from the + // unsnapped State placement policy, at every supported test zoom. + let release = screen(&canvas, Point::new(683.75, 362.25)); + let expected_pin = Position { x: 685., y: 360. }; + motion(&canvas, &mut state, start, release); + assert!(input(&canvas, &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), release).is_none()); + assert!(matches!(state.press, Some(Press::Bend { relation: id }) if id == relation)); + assert!(state.group.is_none()); + motion(&canvas, &mut state, release, release); + let mut expected_route = original.clone(); + expected_route.waypoints[0] = expected_pin; + assert_eq!(state.bend_preview.as_ref(), Some(&expected_route)); + assert_eq!(canvas.scene(&state).unwrap().routed.edges[&relation].pins, vec![expected_pin]); + assert_eq!(f.project.diagrams[&f.diagram].routes[0], original); + if cancel { + let escape = group_key(&canvas, &mut state, keyboard::key::Named::Escape, + keyboard::Modifiers::empty()); + assert!(matches!(escape, Some(Event::Choose(Tool::Select))), + "Escape cancels the bend before it can emit selection Clear: {escape:?}"); + assert!(state.press.is_none() && state.bend.is_none() && state.bend_preview.is_none()); + assert_eq!(canvas.scene(&state).unwrap().routed.edges[&relation].points, + baseline.routed.edges[&relation].points); + assert!(input(&canvas, &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), release).is_none()); + } else { + let event = input(&canvas, &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), release).unwrap(); + assert!(matches!(&event, Event::SetBends { context, relation: id, waypoints } + if *context == canvas.context && *id == relation && *waypoints == vec![expected_pin])); + let plan = state_edit::plan(&f.project, canvas.context, &event).unwrap(); + let actual = prepare_command(&f.project, &plan.command).unwrap().project; + let mut expected = f.project.clone(); + expected.diagrams.get_mut(&f.diagram).unwrap().routes[0] = expected_route; + assert_eq!(actual, expected); + } + assert_eq!(selection.snapshot(selection_context(canvas.context)).unwrap(), snapshot); + assert!(state.group.is_none()); + } + } +} + +#[test] +fn state_singleton_selection_endpoint_reconnect_stationary_cancel_and_commit() { + for outcome in 0..3 { + let mut f = fixture(); + let relation = f.project.relate(RelationKind::StateTransition, "Go standby", f.a, f.b); + let mut canvas = canvas(&f, Tool::Select, 1.); + canvas.selected = relation; + let selection = group_selection(&canvas, [Target::Edge(relation)]); + let snapshot = selection.snapshot(selection_context(canvas.context)).unwrap(); + canvas.selection = Some(&selection); + let mut state = State::default(); + let baseline = canvas.scene(&state).unwrap(); + let handle = screen(&canvas, point(*baseline.routed.edges[&relation].points.last().unwrap())); + let target = screen(&canvas, header(&canvas, f.c)); + motion(&canvas, &mut state, handle, target); + assert!(input(&canvas, &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), target).is_none()); + assert!(matches!(state.press, + Some(Press::Reconnect { relation: id, endpoint: Endpoint::Target, start: Some(at) }) + if id == relation && at == handle)); + assert!(state.group.is_none() && state.bend.is_none()); + if outcome > 0 { motion(&canvas, &mut state, target, target); } + if outcome == 1 { + let escape = group_key(&canvas, &mut state, keyboard::key::Named::Escape, + keyboard::Modifiers::empty()); + assert!(matches!(escape, Some(Event::Choose(Tool::Select))), + "Escape cancels reconnect without clearing the exact edge: {escape:?}"); + assert!(state.press.is_none()); + } + let event = input(&canvas, &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), target); + if outcome < 2 { + assert!(event.is_none(), "stationary/cancelled reconnect produced {event:?}"); + } else { + let event = event.unwrap(); + assert!(matches!(&event, Event::Reconnect { context, relation: id, endpoint: Endpoint::Target, vertex } + if *context == canvas.context && *id == relation && *vertex == f.c)); + let plan = state_edit::plan(&f.project, canvas.context, &event).unwrap(); + let actual = prepare_command(&f.project, &plan.command).unwrap().project; + assert_eq!(plan.selected, relation); + assert_eq!(actual.relations[&relation].source(), f.a); + assert_eq!(actual.relations[&relation].target(), f.c); + assert_eq!(actual.elements, f.project.elements); + } + assert_eq!(f.project.relations[&relation].target(), f.b); + assert_eq!(selection.snapshot(selection_context(canvas.context)).unwrap(), snapshot); + assert!(state.group.is_none() && state.press.is_none()); + } +} + +#[test] +fn state_singleton_selection_shift_handles_toggle_exact_edge_before_route_tools() { + let (f, relation) = bend_fixture(true); + let mut canvas = canvas(&f, Tool::Select, 1.); + canvas.selected = relation; + let selection = group_selection(&canvas, [Target::Edge(relation)]); + canvas.selection = Some(&selection); + let baseline = canvas.scene(&State::default()).unwrap(); + let edge = &baseline.routed.edges[&relation]; + for world in [edge.pins[0], *edge.points.last().unwrap()] { + let mut state = State::default(); + assert!(canvas::Program::update(&canvas, &mut state, + &canvas::Event::Keyboard(keyboard::Event::ModifiersChanged(keyboard::Modifiers::SHIFT)), + bounds(), mouse::Cursor::Unavailable).is_none()); + let at = screen(&canvas, point(world)); + motion(&canvas, &mut state, at, at); + assert!(matches!(input(&canvas, &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), at), + Some(Event::Selection(SelectionEvent::Choose(context, Target::Edge(id), Choice::Toggle))) + if context == selection_context(canvas.context) && id == relation)); + assert!(state.group.is_none() && state.press.is_none() && state.bend.is_none()); + let away = at + Vector::new(35., 20.); + motion(&canvas, &mut state, away, away); + assert!(input(&canvas, &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), away).is_none()); + assert!(state.bend_preview.is_none()); + assert_eq!(canvas.scene(&state).unwrap().routed.edges[&relation].points, edge.points); + } +} + +fn group_selection(canvas: &StateCanvas<'_>, targets: impl IntoIterator) -> Selection { + let mut selection = Selection::new(selection_context(canvas.context).scope); + for target in targets { selection.add(target, |_| true); } + selection +} + +fn group_key(canvas: &StateCanvas<'_>, state: &mut State, key: keyboard::key::Named, + modifiers: keyboard::Modifiers) -> Option +{ + canvas::Program::update(canvas, state, &canvas::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(key), modified_key: keyboard::Key::Named(key), + physical_key: keyboard::key::Physical::Code(keyboard::key::Code::Delete), + location: keyboard::Location::Standard, modifiers, text: None, repeat: false, + }), bounds(), mouse::Cursor::Unavailable).and_then(|action| action.into_inner().0) +} + +#[test] +fn state_group_ordered_press_preview_and_fractional_release_preserve_primary() { + let (f, relation) = bend_fixture(true); + for zoom in [0.5, 1., 2.] { + let mut canvas = canvas(&f, Tool::Select, zoom); + let b = placement(&f, f.b); + let c = placement(&f, f.c); + let selection = group_selection(&canvas, [Target::Placement(b), Target::Placement(c)]); + canvas.selection = Some(&selection); + canvas.selected = f.c; + let before = selection.snapshot(selection_context(canvas.context)).unwrap(); + let mut state = State::default(); + let start = screen(&canvas, header(&canvas, f.b)); + let end = start + Vector::new(7.25 * zoom, 10.5 * zoom); + motion(&canvas, &mut state, start, end); + assert!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), end).is_none()); + assert!(state.group.is_some() && state.press.is_none()); + assert_eq!(selection.primary(), Some(&Target::Placement(c))); + motion(&canvas, &mut state, end, end); + let scene = canvas.scene_with_bounds(&state, Some(bounds())).unwrap(); + for id in [b, c] { + let node = scene.view.nodes.iter().find(|n| n.id == id).unwrap(); + let original = &f.project.diagrams[&f.diagram].nodes[&id]; + assert_eq!(node.position, Position { x: original.position.x + 7.25, y: original.position.y + 10.5 }); + } + assert_eq!(scene.routed.edges[&relation].pins, vec![Position { x: 657.5, y: 351.25 }]); + assert!(matches!(input(&canvas, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), start), + Some(Event::Selection(SelectionEvent::Move(snapshot, Position { x: 7.25, y: 10.5 }))) if snapshot == before)); + assert_eq!(selection.snapshot(selection_context(canvas.context)).unwrap(), before); + } +} + +#[test] +fn state_group_stationary_collapses_but_out_and_back_keeps_group() { + let f = fixture(); + let mut canvas = canvas(&f, Tool::Select, 1.); + let a = placement(&f, f.a); + let b = placement(&f, f.b); + let selection = group_selection(&canvas, [Target::Placement(a), Target::Placement(b)]); + canvas.selection = Some(&selection); + let start = screen(&canvas, header(&canvas, f.a)); + for dragged in [false, true] { + let mut state = State::default(); + motion(&canvas, &mut state, start, start); + assert!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), start).is_none()); + if dragged { + motion(&canvas, &mut state, start + Vector::new(20., 0.), start); + motion(&canvas, &mut state, start, start); + } + let event = input(&canvas, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), start); + if dragged { + assert!(matches!(event, Some(Event::Selection(SelectionEvent::Move(_, Position { x: 0., y: 0. }))))); + } else { + assert!(matches!(event, Some(Event::Selection(SelectionEvent::Collapse(_, Target::Placement(id)))) if id == a)); + } + } +} + +#[test] +fn state_group_changed_authority_viewport_or_membership_retires_preview_and_release() { + let f = fixture(); + for change in 0..7 { + let mut canvas = canvas(&f, Tool::Select, 1.); + let a = placement(&f, f.a); + let b = placement(&f, f.b); + let selection = group_selection(&canvas, [Target::Placement(a), Target::Placement(b)]); + let mut replacement = group_selection(&canvas, [Target::Placement(a), Target::Placement(b)]); + replacement.remove(&Target::Placement(a)); + replacement.add(Target::Placement(a), |_| true); + canvas.selection = Some(&selection); + let mut state = State::default(); + let start = screen(&canvas, header(&canvas, f.a)); + motion(&canvas, &mut state, start, start); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), start); + motion(&canvas, &mut state, start + Vector::new(12., 8.), start); + assert!(canvas.group_preview_key(&state, Some(bounds())).is_some()); + let mut changed_bounds = bounds(); + match change { + 0 => canvas.context.revision += 1, + 1 => canvas.context.epoch += 1, + 2 => canvas.context.document = Id::new_v4(), + 3 => canvas.zoom = 1.1, + 4 => canvas.pan.x += 1., + 5 => changed_bounds.x += 1., + _ => canvas.selection = Some(&replacement), + } + assert!(canvas.group_preview_key(&state, Some(changed_bounds)).is_none()); + if change < 3 { + assert!(group_key(&canvas, &mut state, keyboard::key::Named::Delete, keyboard::Modifiers::empty()).is_none()); + } + assert!(input_at_bounds(&canvas, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), start, changed_bounds).is_none()); + assert!(state.group.is_none()); + } +} + +#[test] +fn state_group_mixed_route_handle_refuses_drag_and_preserves_stationary_collapse() { + let (f, relation) = bend_fixture(true); + let mut canvas = canvas(&f, Tool::Select, 1.); + let selection = group_selection(&canvas, [Target::Placement(placement(&f, f.b)), Target::Edge(relation)]); + canvas.selection = Some(&selection); + canvas.selected = relation; + let scene = canvas.scene(&State::default()).unwrap(); + let at = screen(&canvas, point(scene.routed.edges[&relation].pins[0])); + for dragged in [false, true] { + let mut state = State::default(); + motion(&canvas, &mut state, at, at); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at); + assert!(state.group.is_some() && state.bend.is_none() && state.press.is_none()); + if dragged { + motion(&canvas, &mut state, at + Vector::new(25., 10.), at); + motion(&canvas, &mut state, at, at); + } + let event = input(&canvas, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), at); + if dragged { + assert!(matches!(event, Some(Event::Selection(SelectionEvent::Refuse(_, _))))); + } else { + assert!(matches!(event, Some(Event::Selection(SelectionEvent::Collapse(_, Target::Edge(id)))) if id == relation)); + } + assert!(state.bend_preview.is_none()); + } +} + +#[test] +fn state_group_shift_row_hit_menu_and_reassignment_keep_exact_identity() { + let mut f = fixture(); + let work = f.project.add_element(Kind::Function, Layer::Sa, "Action work", None).unwrap(); + let action = f.project.relate(RelationKind::StateAction, "Entry", f.a, work); + let internal = f.project.relate(RelationKind::StateTransition, "Internal", f.a, f.a); + if let RelationBody::StateTransition { transition_kind, .. } = &mut f.project.relations.get_mut(&internal).unwrap().body { + *transition_kind = TransitionKind::Internal; + } + let a = placement(&f, f.a); + for relation in [action, internal] { + let mut canvas = canvas(&f, Tool::Select, 1.); + let selection = group_selection(&canvas, [Target::Placement(placement(&f, f.b))]); + canvas.selection = Some(&selection); + let scene = canvas.scene(&State::default()).unwrap(); + let node = scene.view.nodes.iter().find(|n| n.id == a).unwrap(); + let rect = state_action_rows(&f.project, node).into_iter().chain(internal_rows(&f.project, node)) + .find(|(row, _)| row.id == relation).unwrap().1; + assert!(visible_row_target(&f.project, f.diagram, &scene.view, a, relation)); + let target = Target::StateRow { placement: a, relation }; + assert_eq!(canvas.hit(&scene, rect.center()).unwrap().target(), target); + let mut state = State::default(); + let modifiers = canvas::Program::update(&canvas, &mut state, + &canvas::Event::Keyboard(keyboard::Event::ModifiersChanged(keyboard::Modifiers::SHIFT)), + bounds(), mouse::Cursor::Unavailable); + assert!(modifiers.is_none()); + let at = screen(&canvas, rect.center()); + motion(&canvas, &mut state, at, at); + assert!(matches!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at), + Some(Event::Selection(SelectionEvent::Choose(_, actual, Choice::Toggle))) if actual == target)); + assert!(matches!(input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Right), at), + Some(Event::ContextMenu { object, placement: None, selection_target: Some(actual), .. }) if object == relation && actual == target)); + canvas::Program::update(&canvas, &mut state, &canvas::Event::Window(iced::window::Event::Unfocused), bounds(), mouse::Cursor::Unavailable); + assert!(state.modifiers.is_empty()); + } + if let RelationBody::StateAction { source, .. } = &mut f.project.relations.get_mut(&action).unwrap().body { *source = f.b; } + if let RelationBody::StateTransition { source, target, .. } = &mut f.project.relations.get_mut(&internal).unwrap().body { *source = f.b; *target = f.b; } + let view = projection::project_diagram(&f.project, &f.project.diagrams[&f.diagram]); + for relation in [action, internal] { + assert!(!visible_row_target(&f.project, f.diagram, &view, a, relation)); + assert!(visible_row_target(&f.project, f.diagram, &view, placement(&f, f.b), relation)); + } +} + +#[test] +fn state_group_delete_and_escape_respect_held_gesture_and_full_set() { + let f = fixture(); + let mut canvas = canvas(&f, Tool::Select, 1.); + let selection = group_selection(&canvas, [Target::Placement(placement(&f, f.a)), Target::Placement(placement(&f, f.b))]); + canvas.selection = Some(&selection); + let mut state = State::default(); + use keyboard::key::Named; + assert!(matches!(group_key(&canvas, &mut state, Named::Delete, keyboard::Modifiers::empty()), Some(Event::Selection(SelectionEvent::Hide(_))))); + assert!(matches!(group_key(&canvas, &mut state, Named::Delete, keyboard::Modifiers::SHIFT), Some(Event::Selection(SelectionEvent::Refuse(_, _))))); + let at = screen(&canvas, header(&canvas, f.a)); + motion(&canvas, &mut state, at, at); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at); + assert!(group_key(&canvas, &mut state, Named::Delete, keyboard::Modifiers::empty()).is_none()); + assert!(state.group.is_some()); + assert!(group_key(&canvas, &mut state, Named::Escape, keyboard::Modifiers::empty()).is_none()); + assert!(state.group.is_none()); + assert!(matches!(group_key(&canvas, &mut state, Named::Escape, keyboard::Modifiers::empty()), Some(Event::Selection(SelectionEvent::Clear(_))))); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Middle), at); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at); + assert!(state.group.is_none() && state.press.is_none() && state.pan_from.is_some()); + assert!(matches!(motion(&canvas, &mut state, at + Vector::new(20., 10.), at), Some(Event::Pan(_, _)))); +} + +#[test] +fn state_group_entry_anchor_uses_authored_position_with_unsnapped_delta() { + let mut f = fixture(); + let inner = f.project.elements[&f.leaf].parent.unwrap(); + let entry = f.project.add_element(Kind::EntryPoint, Layer::Sa, "Enter", Some(inner)).unwrap(); + let original = Position { x: 450.25, y: 410.75 }; + let entry_node = f.project.place(f.diagram, entry, original); + let mut canvas = canvas(&f, Tool::Select, 1.); + let selection = group_selection(&canvas, [Target::Placement(entry_node), Target::Placement(placement(&f, f.leaf))]); + canvas.selection = Some(&selection); + let scene = canvas.scene(&State::default()).unwrap(); + let shown = scene.view.nodes.iter().find(|n| n.id == entry_node).unwrap(); + assert_ne!(shown.position.x, original.x); + let at = screen(&canvas, rectangle(shown).center()); + let mut state = State::default(); + motion(&canvas, &mut state, at, at); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at); + assert!(state.group.is_some()); + let end = at + Vector::new(7.25, 8.5); + motion(&canvas, &mut state, end, end); + assert!(matches!(input(&canvas, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), end), + Some(Event::Selection(SelectionEvent::Move(_, Position { x: 7.25, y: 8.5 }))))); + assert_eq!(f.project.diagrams[&f.diagram].nodes[&entry_node].position, original); +} + +#[test] +fn state_group_row_and_port_mixtures_refuse_complete_set_without_owner_promotion() { + let mut f = fixture(); + let work = f.project.add_element(Kind::Function, Layer::Sa, "Action work", None).unwrap(); + let action = f.project.relate(RelationKind::StateAction, "Entry", f.a, work); + let a = placement(&f, f.a); + for other in [Target::StateRow { placement: a, relation: action }, Target::Port { placement: a, port: work }] { + let mut canvas = canvas(&f, Tool::Select, 1.); + let selection = group_selection(&canvas, [Target::Placement(a), other]); + canvas.selection = Some(&selection); + let mut state = State::default(); + assert!(matches!(group_key(&canvas, &mut state, keyboard::key::Named::Delete, keyboard::Modifiers::empty()), + Some(Event::Selection(SelectionEvent::Refuse(_, _))))); + let at = screen(&canvas, header(&canvas, f.a)); + motion(&canvas, &mut state, at, at); + input(&canvas, &mut state, mouse::Event::ButtonPressed(mouse::Button::Left), at); + assert!(state.group.as_ref().is_some_and(|g| g.movement.is_none())); + let end = at + Vector::new(12., 8.); + motion(&canvas, &mut state, end, end); + assert!(canvas.group_preview_key(&state, Some(bounds())).is_none()); + assert!(matches!(input(&canvas, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), end), + Some(Event::Selection(SelectionEvent::Refuse(snapshot, _))) if snapshot.targets().len() == 2)); + assert!(state.press.is_none() && state.preview.is_none()); + } +} diff --git a/crates/oa-desktop/src/behavior/state_view.rs b/crates/oa-desktop/src/behavior/state_view.rs index 0c17c400..e7d5785f 100644 --- a/crates/oa-desktop/src/behavior/state_view.rs +++ b/crates/oa-desktop/src/behavior/state_view.rs @@ -41,7 +41,10 @@ pub fn diagram(app: &App, machine: Id) -> Option { } fn event(e: Event) -> Message { - Message::StateDiagram(e) + match e { + Event::Selection(selection) => Message::DiagramSelection(selection), + other => Message::StateDiagram(other), + } } pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Element<'a, Message> { @@ -143,8 +146,9 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem .collect::>(); // Keep the optional picker in its own stable slot. Inserting it as a // sibling before the canvas would replace the positional Iced focus tree. - let missing_spacing = if missing.is_empty() { 0.0_f32 } else { 8.0 }; - let mut missing_controls = Column::new(); + // Reserve the picker height even when all elements are placed. Group Hide + // and Undo must not resize the canvas and silently refit its camera. + let mut missing_controls = Column::new().height(34); if !missing.is_empty() { let position = Position { x: (48. - state.pan.x) / state.zoom, @@ -169,6 +173,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem diagram: id, context, selected: app.selected, + selection: app.diagram_selection.as_ref(), navigation_focus: app .diagram_focus(crate::diagram_usage::Surface::Saved(id)) .map(|usage| usage.bounds), @@ -183,6 +188,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem out = out.push( column![ missing_controls, + selection_controls(app), crate::diagram::focus::wrap( canvas.map(event), context, @@ -191,7 +197,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem Message::DiagramTraverse, ) ] - .spacing(missing_spacing) + .spacing(8) .height(Fill), ); let base: Element<'_, Message> = container(out.spacing(8)) @@ -202,6 +208,47 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem with_form(app, machine, base) } +fn selection_controls(app: &App) -> Element<'_, Message> { + use crate::app::multiselect::{Event as SelectionEvent, placements}; + let snapshot = app.multiselect_snapshot(); + let count = snapshot.as_ref().map_or(0, |s| s.targets().len()); + let mut controls = row![label(format!("{count} selected"))] + .spacing(7) + .align_y(iced::Center) + .height(32); + if let Some(snapshot) = snapshot.filter(|s| s.targets().len() > 1) { + if placements(&snapshot).is_some() { + controls = controls.push(action( + "Hide selected", + Message::DiagramSelection(SelectionEvent::Hide(snapshot.clone())), + )); + } + controls = controls + .push(action( + "Clear selection", + Message::DiagramSelection(SelectionEvent::Clear(snapshot.context())), + )) + .push( + label(format!( + "Properties edit only {}", + crate::views::short(app.project().named(app.selected), 18) + )) + .wrapping(iced::widget::text::Wrapping::None), + ); + } + let hint = if app.behavior.diagram.tool == Tool::Select { + "Shift+click adds or removes from selection" + } else { + "Use Select / move to select multiple elements" + }; + column![ + controls, + label(hint).wrapping(iced::widget::text::Wrapping::None) + ] + .height(56) + .into() +} + fn transition_details(machine: Id, selected: Id) -> Element<'static, Message> { action( "New transition with details…", diff --git a/crates/oa-desktop/src/diagram.rs b/crates/oa-desktop/src/diagram.rs index a1de43a1..8c8bacc2 100644 --- a/crates/oa-desktop/src/diagram.rs +++ b/crates/oa-desktop/src/diagram.rs @@ -39,12 +39,17 @@ mod port_labels; pub use port_labels::PortLabels; #[path = "diagram/focus.rs"] pub(crate) mod focus; +pub(crate) mod group_gesture; +mod multiselect; +#[cfg(test)] +mod multiselect_tests; #[path = "diagram/work_drag.rs"] mod work_drag; pub use work_drag::WorkFeedback; #[derive(Clone, Copy)] pub struct DiagramCanvas<'a> { + pub selection: Option<&'a crate::diagram_selection::Selection>, pub allocation: Option>, pub project: &'a Project, pub view_index: Option<&'a oa_core::view_options::ViewIndex>, @@ -63,6 +68,8 @@ pub struct DiagramCanvas<'a> { } #[derive(Default)] pub struct State { + modifiers: iced::keyboard::Modifiers, + group: Option, work: Option>, work_cache: RefCell>>, pointer: Option, @@ -462,6 +469,7 @@ impl DiagramCanvas<'_> { } fn preview_diagram(&self, state: &State) -> Diagram { let mut diagram = self.project.diagrams[&self.diagram].clone(); + self.group_preview(state, &mut diagram); if let (Some((id, _, original)), Some(preview)) = (state.drag, state.preview) { let delta = Position { x: preview.x - original.x, @@ -604,6 +612,13 @@ impl canvas::Program for DiagramCanvas<'_> { bounds: Rectangle, cursor: mouse::Cursor, ) -> Option> { + 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(); + } // An attachment belongs to the boundary geometry captured at press. // Camera/layout changes must not reinterpret its pointer in another space. if matches!( @@ -641,6 +656,7 @@ impl canvas::Program for DiagramCanvas<'_> { return Some(action); } if state.context != Some(self.context) { + state.group = None; state.work = None; state.pointer = None; state.port_drag = None; @@ -707,6 +723,9 @@ impl canvas::Program for DiagramCanvas<'_> { let local = cursor .position() .map(|p| Point::new(p.x - bounds.x, p.y - bounds.y)); + if let Some(action) = self.group_input(state, event, bounds, cursor) { + return Some(action); + } match event { canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) if cursor.is_over(bounds) => @@ -1262,7 +1281,17 @@ impl canvas::Program for DiagramCanvas<'_> { .collect::>(); for n in &view.nodes { if containers.contains(&n.id) { - paint_node(&mut f, project, n, self.selected, true); + paint_node( + &mut f, + project, + n, + if self.selection_contains(crate::diagram_usage::Target::Placement(n.id)) { + n.element + } else { + self.selected + }, + true, + ); } } let highlighted = highlighted_exchanges(project, self.selected); @@ -1274,7 +1303,9 @@ impl canvas::Program for DiagramCanvas<'_> { continue; }; let end = point(end); - let active = edge.id == self.selected || highlighted.contains(&edge.id); + let active = edge.id == self.selected + || highlighted.contains(&edge.id) + || self.selection_contains(crate::diagram_usage::Target::Edge(edge.id)); let color = if active { style::TEAL } else if routed @@ -1370,17 +1401,42 @@ impl canvas::Program for DiagramCanvas<'_> { } for n in &view.nodes { if !containers.contains(&n.id) { - paint_node(&mut f, project, n, self.selected, false); + paint_node( + &mut f, + project, + n, + if self.selection_contains(crate::diagram_usage::Target::Placement(n.id)) { + n.element + } else { + self.selected + }, + false, + ); } else { // Container headers stay readable when relationships traverse // the allocation enclosure. Children remain on top. let mut header = n.clone(); header.size.height = 62.; - paint_node(&mut f, project, &header, self.selected, true); + paint_node( + &mut f, + project, + &header, + if self.selection_contains(crate::diagram_usage::Target::Placement(n.id)) { + n.element + } else { + self.selected + }, + true, + ); } } for port in &view.ports { - let color = if self.selected == port.element { + let selected = self.selected == port.element + || self.selection_contains(crate::diagram_usage::Target::Port { + port: port.element, + placement: port.owner, + }); + let color = if selected { style::TEAL } else { style::kind_color( @@ -1408,11 +1464,7 @@ impl canvas::Program for DiagramCanvas<'_> { let mut stroke = Stroke::default() .with_color(color) - .with_width(if self.selected == port.element { - 2.5 - } else { - 1.5 - }); + .with_width(if selected { 2.5 } else { 1.5 }); if glyph == PortGlyph::Legacy { stroke.line_dash = canvas::LineDash { segments: &[3., 2.], @@ -2277,6 +2329,7 @@ mod tests { y: original.y - 200., }; let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, context: Context { @@ -2354,6 +2407,7 @@ mod tests { .unwrap() .id; let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, context: Context { @@ -2482,6 +2536,7 @@ mod tests { }; let session = Session::new(&p, &view, context, Tool::FunctionalExchange); let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -2657,6 +2712,7 @@ mod tests { for zoom in [0.5, 1., 1.5] { let (p, diagram, edge) = route_fixture(); let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -2735,6 +2791,7 @@ mod tests { fn fast_drags_commit_the_release_cursor_without_a_final_motion_event() { let (p, diagram, edge) = route_fixture(); let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -2834,6 +2891,7 @@ mod tests { fn route_cache_ignores_pan_zoom_and_cancelled_drag_cannot_commit() { let (p, diagram, edge) = route_fixture(); let mut canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -2964,6 +3022,7 @@ mod tests { assert!(!svg.contains("data-navigation=\"source\"")); assert!(svg.contains("data-navigation=\"target\"")); let mut canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -3020,6 +3079,7 @@ mod tests { offset: Position { x: 0., y: -160. }, }); let mut canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -3096,6 +3156,7 @@ mod tests { }, }); let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, @@ -3131,6 +3192,7 @@ mod tests { routed.edges.insert(duplicate, duplicate_route); let selected = edge.max(duplicate); // UUID ordering must not override selection. let canvas = DiagramCanvas { + selection: None, project: &p, view_index: None, diagram, diff --git a/crates/oa-desktop/src/diagram/attachment_tests.rs b/crates/oa-desktop/src/diagram/attachment_tests.rs index 8e9b97b1..974df43f 100644 --- a/crates/oa-desktop/src/diagram/attachment_tests.rs +++ b/crates/oa-desktop/src/diagram/attachment_tests.rs @@ -34,6 +34,7 @@ fn fixture() -> App { fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> { DiagramCanvas { + selection: None, allocation: None, project: app.project(), view_index: None, diff --git a/crates/oa-desktop/src/diagram/focus.rs b/crates/oa-desktop/src/diagram/focus.rs index 5193d529..3c93e87e 100644 --- a/crates/oa-desktop/src/diagram/focus.rs +++ b/crates/oa-desktop/src/diagram/focus.rs @@ -12,6 +12,8 @@ struct Input { suspended: bool, traversing: bool, pointer: Option, + modifiers: iced::keyboard::Modifiers, + restore_modifiers: bool, } impl operation::Focusable for Input { fn is_focused(&self) -> bool { @@ -95,6 +97,7 @@ impl Widget for Owner<'_, Mes // canvas's transient gestures without retiring this focus owner. state.pointer = None; tree.children[0] = Tree::new(self.content.as_widget()); + state.restore_modifiers = true; } else { tree.diff_children(std::slice::from_ref(&self.content)); } @@ -143,6 +146,14 @@ impl Widget for Owner<'_, Mes viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); + if let InputEvent::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers)) = event { + state.modifiers = *modifiers; + state.restore_modifiers = false; + } + if matches!(event, InputEvent::Window(iced::window::Event::Unfocused)) { + state.modifiers = iced::keyboard::Modifiers::default(); + state.restore_modifiers = true; + } if let InputEvent::Mouse(mouse::Event::CursorMoved { position }) = event { state.pointer = Some(*position); } @@ -171,7 +182,14 @@ impl Widget for Owner<'_, Mes } // Save/export I/O denies commands and acquisition, while real focus // operations and outside clicks can still revoke the retained owner. - if self.suspended && matches!(event, InputEvent::Keyboard(_) | InputEvent::Mouse(_)) { + let passive_modifiers = matches!( + event, + InputEvent::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) + ); + if self.suspended + && !passive_modifiers + && matches!(event, InputEvent::Keyboard(_) | InputEvent::Mouse(_)) + { return; } if let InputEvent::Keyboard(iced::keyboard::Event::KeyPressed { @@ -193,13 +211,30 @@ impl Widget for Owner<'_, Mes return; } if matches!(event, InputEvent::Keyboard(_)) + && !passive_modifiers && (!state.focused || state.traversing || !self.enabled) { return; } - if shell.is_event_captured() { + if shell.is_event_captured() && !passive_modifiers { return; } + if state.restore_modifiers { + // Suspending I/O replaces the child to retire pointer gestures. + // Held modifiers belong to this longer-lived passive owner, so + // restore them before the first admitted event reaches the child. + state.restore_modifiers = false; + self.content.as_widget_mut().update( + &mut tree.children[0], + &InputEvent::Keyboard(iced::keyboard::Event::ModifiersChanged(state.modifiers)), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } self.content.as_widget_mut().update( &mut tree.children[0], event, diff --git a/crates/oa-desktop/src/diagram/group_gesture.rs b/crates/oa-desktop/src/diagram/group_gesture.rs new file mode 100644 index 00000000..7e8a3bf8 --- /dev/null +++ b/crates/oa-desktop/src/diagram/group_gesture.rs @@ -0,0 +1,685 @@ +//! Saved-view pointer geometry. Selection, capability and model admission remain +//! with the caller; this module never applies a selection or a model command. +use crate::diagram_selection::Snapshot; +use crate::diagram_usage::{Surface, Target}; +use iced::{Point, Rectangle, Vector}; +use oa_core::{Id, Position}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct Viewport { + pub zoom: f32, + pub pan: Vector, + pub bounds: Rectangle, +} + +/// Node displacement policy; route-bend snapping remains outside this helper. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DisplacementPolicy { + /// Preserve the ordinary saved-view ten-unit anchor grid. + Grid10, + /// Preserve State views' fractional authored placement displacement. + Unsnapped, +} + +#[derive(Debug)] +pub(crate) struct Gesture { + snapshot: Snapshot, + anchor: Id, + original: Position, + press: Point, + viewport: Viewport, + displacement: DisplacementPolicy, + crossed_threshold: bool, + delta: Position, + retired: bool, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum Outcome { + Click(Id), + Move(Snapshot, Position), +} + +impl Gesture { + pub fn new( + snapshot: Snapshot, + anchor: Id, + original: Position, + press: Point, + viewport: Viewport, + ) -> Option { + Self::new_with_policy( + snapshot, + anchor, + original, + press, + viewport, + DisplacementPolicy::Grid10, + ) + } + + /// `original` is the authored placement position, not its projected symbol + /// position (State entry/exit symbols can be displayed on another boundary). + /// Pointer displacement comes from frozen screen geometry and zoom. + pub fn new_with_policy( + snapshot: Snapshot, + anchor: Id, + original: Position, + press: Point, + viewport: Viewport, + displacement: DisplacementPolicy, + ) -> Option { + if !matches!(snapshot.context().scope.surface, Surface::Saved(_)) + || !snapshot.targets().contains(&Target::Placement(anchor)) + || !snapshot + .targets() + .iter() + .all(|target| matches!(target, Target::Placement(_))) + || !viewport.valid() + || !finite_position(original) + || !finite_point(press) + || !viewport.bounds.contains(press) + { + return None; + } + Some(Self { + snapshot, + anchor, + original, + press, + viewport, + displacement, + crossed_threshold: false, + delta: Position { x: 0., y: 0. }, + retired: false, + }) + } + + pub fn snapshot(&self) -> &Snapshot { + &self.snapshot + } + + #[cfg(test)] + pub fn anchor(&self) -> Id { + self.anchor + } + + /// Compare before processing each event. On mismatch, the caller must drop + /// this gesture, just as on Escape, focus loss or changed model authority. + pub fn viewport_matches(&self, viewport: Viewport) -> bool { + !self.retired && self.viewport == viewport + } + + pub fn crossed_threshold(&self) -> bool { + self.crossed_threshold + } + + /// Common policy-adjusted world delta for preview and release; zero before a drag. + pub fn delta(&self) -> Position { + self.delta + } + + /// Process an absolute screen position. False permanently retires the + /// gesture; the caller must remove its preview and produce no operation. + pub fn move_to(&mut self, pointer: Point) -> bool { + if self.retired { + return false; + } + if !finite_point(pointer) || !self.viewport.bounds.contains(pointer) { + self.retire(); + return false; + } + let screen = pointer - self.press; + self.crossed_threshold |= screen.x.hypot(screen.y) > 3.; + if !self.crossed_threshold { + return true; + } + let unsnapped = Position { + x: screen.x / self.viewport.zoom, + y: screen.y / self.viewport.zoom, + }; + let desired = Position { + x: self.original.x + unsnapped.x, + y: self.original.y + unsnapped.y, + }; + if !finite_position(desired) { + self.retire(); + return false; + } + // Returning to the original position must not align an off-grid anchor + // as an accidental edit. It is still a drag, so selection stays grouped. + let delta = match self.displacement { + // Use the pointer delta directly, avoiding precision loss from + // subtracting a large authored original from its desired position. + DisplacementPolicy::Unsnapped => unsnapped, + DisplacementPolicy::Grid10 if desired == self.original => Position { x: 0., y: 0. }, + DisplacementPolicy::Grid10 => Position { + x: (desired.x / 10.).round() * 10. - self.original.x, + y: (desired.y / 10.).round() * 10. - self.original.y, + }, + }; + if !finite_position(delta) { + self.retire(); + return false; + } + self.delta = delta; + true + } + + /// Consume the final pointer too: a release can be the first displaced + /// event. The caller must validate snapshot and viewport before acting on + /// either outcome. Cancelling is dropping the gesture without calling this. + pub fn release(mut self, pointer: Point) -> Option { + if !self.move_to(pointer) { + return None; + } + Some(if self.crossed_threshold { + Outcome::Move(self.snapshot, self.delta) + } else { + Outcome::Click(self.anchor) + }) + } + + fn retire(&mut self) { + self.retired = true; + self.delta = Position { x: 0., y: 0. }; + } +} + +impl Viewport { + fn valid(self) -> bool { + self.zoom.is_finite() + && self.zoom > 0. + && self.pan.x.is_finite() + && self.pan.y.is_finite() + && self.bounds.x.is_finite() + && self.bounds.y.is_finite() + && self.bounds.width.is_finite() + && self.bounds.height.is_finite() + && self.bounds.width > 0. + && self.bounds.height > 0. + && (self.bounds.x + self.bounds.width).is_finite() + && (self.bounds.y + self.bounds.height).is_finite() + } +} + +fn finite_point(point: Point) -> bool { + point.x.is_finite() && point.y.is_finite() +} + +fn finite_position(position: Position) -> bool { + position.x.is_finite() && position.y.is_finite() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::diagram_selection::{Context, Scope, Selection}; + + fn id(value: u128) -> Id { + Id::from_u128(value) + } + + fn context() -> Context { + Context { + scope: Scope { + document: id(1), + surface: Surface::Saved(id(2)), + }, + revision: 3, + epoch: 4, + } + } + + fn snapshot(targets: impl IntoIterator) -> Snapshot { + let mut selection = Selection::new(context().scope); + for target in targets { + selection.add(target, |_| true); + } + selection.snapshot(context()).unwrap() + } + + fn viewport() -> Viewport { + Viewport { + zoom: 1., + pan: Vector::new(17., -8.), + bounds: Rectangle { + x: 100., + y: 50., + width: 400., + height: 300., + }, + } + } + + fn press() -> Point { + Point::new(150., 120.) + } + + fn gesture(original: Position) -> Gesture { + Gesture::new( + snapshot([Target::Placement(id(10)), Target::Placement(id(11))]), + id(10), + original, + press(), + viewport(), + ) + .unwrap() + } + + #[test] + fn stationary_or_exact_threshold_release_is_click_without_snapping() { + for pointer in [press(), Point::new(153., 120.), Point::new(152., 122.)] { + let mut gesture = gesture(Position { x: 13., y: -7. }); + assert!(gesture.move_to(pointer)); + assert!(!gesture.crossed_threshold()); + assert_eq!(gesture.delta(), Position { x: 0., y: 0. }); + assert_eq!(gesture.release(pointer), Some(Outcome::Click(id(10)))); + } + } + + #[test] + fn threshold_latches_and_return_to_offgrid_origin_is_zero_move() { + let mut gesture = gesture(Position { x: 13., y: -7. }); + let captured = gesture.snapshot().clone(); + assert!(gesture.move_to(Point::new(170., 140.))); + assert!(gesture.crossed_threshold()); + assert!(gesture.move_to(press())); + assert!(gesture.crossed_threshold()); + assert_eq!(gesture.delta(), Position { x: 0., y: 0. }); + assert_eq!( + gesture.release(press()), + Some(Outcome::Move(captured, Position { x: 0., y: 0. })) + ); + } + + #[test] + fn release_only_displacement_that_snaps_to_zero_still_preserves_group() { + let gesture = gesture(Position { x: 20., y: 40. }); + let captured = gesture.snapshot().clone(); + assert_eq!( + gesture.release(Point::new(154., 120.)), + Some(Outcome::Move(captured, Position { x: 0., y: 0. })) + ); + } + + #[test] + fn offgrid_anchor_yields_one_shared_delta_and_release_matches_preview() { + let mut gesture = gesture(Position { x: 13., y: -7. }); + let captured = gesture.snapshot().clone(); + assert_eq!(gesture.anchor(), id(10)); + let pointer = Point::new(167., 133.); + assert!(gesture.move_to(pointer)); + // Desired anchor (30,6) snaps to (30,10), hence shared delta (17,17). + assert_eq!(gesture.delta(), Position { x: 17., y: 17. }); + let sibling = Position { x: -6., y: 24. }; + let translated = Position { + x: sibling.x + gesture.delta().x, + y: sibling.y + gesture.delta().y, + }; + assert_eq!(translated, Position { x: 11., y: 41. }); + assert_eq!( + gesture.release(pointer), + Some(Outcome::Move(captured, Position { x: 17., y: 17. })) + ); + } + + #[test] + fn zoom_scales_world_delta_but_threshold_remains_in_screen_pixels() { + let mut gesture = Gesture::new( + snapshot([Target::Placement(id(10))]), + id(10), + Position { x: 20., y: 40. }, + press(), + Viewport { + zoom: 2., + ..viewport() + }, + ) + .unwrap(); + assert!(gesture.move_to(Point::new(153., 120.))); + assert!(!gesture.crossed_threshold()); + assert!(gesture.move_to(Point::new(168., 112.))); + // Screen (18,-8) means world (9,-4), snapped anchor (30,40). + assert_eq!(gesture.delta(), Position { x: 10., y: 0. }); + assert!(gesture.crossed_threshold()); + } + + #[test] + fn unsnapped_fractional_pointer_delta_preserves_authored_offset() { + let authored = Position { + x: 1024.125, + y: -321.5, + }; + let mut gesture = Gesture::new_with_policy( + snapshot([Target::Placement(id(10)), Target::Placement(id(11))]), + id(10), + authored, + press(), + Viewport { + zoom: 2., + ..viewport() + }, + DisplacementPolicy::Unsnapped, + ) + .unwrap(); + let captured = gesture.snapshot().clone(); + let pointer = Point::new(164.5, 114.5); + assert!(gesture.move_to(pointer)); + assert_eq!(gesture.delta(), Position { x: 7.25, y: -2.75 }); + let translated_authored = Position { + x: authored.x + gesture.delta().x, + y: authored.y + gesture.delta().y, + }; + assert_eq!( + translated_authored, + Position { + x: 1031.375, + y: -324.25 + } + ); + assert_eq!( + gesture.release(pointer), + Some(Outcome::Move(captured, Position { x: 7.25, y: -2.75 })), + ); + } + + #[test] + fn unsnapped_threshold_and_return_to_origin_preserve_group_lifecycle() { + let make = || { + Gesture::new_with_policy( + snapshot([Target::Placement(id(10)), Target::Placement(id(11))]), + id(10), + Position { x: 13.25, y: -7.5 }, + press(), + viewport(), + DisplacementPolicy::Unsnapped, + ) + .unwrap() + }; + let mut click = make(); + assert!(click.move_to(Point::new(153., 120.))); + assert!(!click.crossed_threshold()); + assert_eq!(click.delta(), Position { x: 0., y: 0. }); + assert_eq!( + click.release(Point::new(153., 120.)), + Some(Outcome::Click(id(10))) + ); + let mut drag = make(); + let captured = drag.snapshot().clone(); + assert!(drag.move_to(Point::new(157.25, 117.25))); + assert_eq!(drag.delta(), Position { x: 7.25, y: -2.75 }); + assert!(drag.move_to(press())); + assert!(drag.crossed_threshold()); + assert_eq!(drag.delta(), Position { x: 0., y: 0. }); + assert_eq!( + drag.release(press()), + Some(Outcome::Move(captured, Position { x: 0., y: 0. })) + ); + } + + #[test] + fn explicit_grid_and_default_constructor_keep_existing_quantization() { + let original = Position { x: 13., y: -7. }; + let mut default = gesture(original); + let mut explicit = Gesture::new_with_policy( + default.snapshot().clone(), + id(10), + original, + press(), + viewport(), + DisplacementPolicy::Grid10, + ) + .unwrap(); + let pointer = Point::new(167., 133.); + assert!(default.move_to(pointer)); + assert!(explicit.move_to(pointer)); + // Existing ordinary behavior: desired (30,6), grid (30,10). + assert_eq!(default.delta(), Position { x: 17., y: 17. }); + assert_eq!(explicit.delta(), Position { x: 17., y: 17. }); + assert_eq!(default.release(pointer), explicit.release(pointer)); + } + + #[test] + fn unsnapped_release_only_movement_and_invalid_pointer_retirement() { + let make = || { + Gesture::new_with_policy( + snapshot([Target::Placement(id(10))]), + id(10), + Position { + x: 20.125, + y: 40.25, + }, + press(), + viewport(), + DisplacementPolicy::Unsnapped, + ) + .unwrap() + }; + let release_only = make(); + let captured = release_only.snapshot().clone(); + assert_eq!( + release_only.release(Point::new(154.25, 120.)), + Some(Outcome::Move(captured, Position { x: 4.25, y: 0. })), + ); + for invalid in [Point::new(f32::NAN, 120.), Point::new(501., 120.)] { + let mut gesture = make(); + assert!(gesture.move_to(Point::new(154.25, 120.))); + assert!(!gesture.move_to(invalid)); + assert!(!gesture.viewport_matches(viewport())); + assert_eq!(gesture.delta(), Position { x: 0., y: 0. }); + assert_eq!(gesture.release(press()), None); + } + } + + #[test] + fn full_typed_selection_and_saved_surface_are_required() { + for targets in [ + vec![], + vec![Target::Placement(id(11))], + vec![Target::Placement(id(10)), Target::Edge(id(20))], + vec![ + Target::Placement(id(10)), + Target::Port { + port: id(20), + placement: id(10), + }, + ], + ] { + assert!( + Gesture::new( + snapshot(targets), + id(10), + Position { x: 0., y: 0. }, + press(), + viewport() + ) + .is_none() + ); + } + let other = Context { + scope: Scope { + surface: Surface::Process(id(2)), + ..context().scope + }, + ..context() + }; + let mut selection = Selection::new(other.scope); + selection.add(Target::Placement(id(10)), |_| true); + assert!( + Gesture::new( + selection.snapshot(other).unwrap(), + id(10), + Position { x: 0., y: 0. }, + press(), + viewport() + ) + .is_none() + ); + } + + #[test] + fn any_frozen_viewport_change_is_detected() { + let gesture = gesture(Position { x: 0., y: 0. }); + assert!(gesture.viewport_matches(viewport())); + for changed in [ + Viewport { + zoom: 2., + ..viewport() + }, + Viewport { + pan: Vector::new(18., -8.), + ..viewport() + }, + Viewport { + pan: Vector::new(17., -7.), + ..viewport() + }, + Viewport { + bounds: Rectangle { + x: 101., + ..viewport().bounds + }, + ..viewport() + }, + Viewport { + bounds: Rectangle { + y: 51., + ..viewport().bounds + }, + ..viewport() + }, + Viewport { + bounds: Rectangle { + width: 401., + ..viewport().bounds + }, + ..viewport() + }, + Viewport { + bounds: Rectangle { + height: 301., + ..viewport().bounds + }, + ..viewport() + }, + ] { + assert!(!gesture.viewport_matches(changed)); + } + } + + #[test] + fn invalid_initial_geometry_cannot_start() { + let saved = snapshot([Target::Placement(id(10))]); + for invalid in [ + Viewport { + zoom: 0., + ..viewport() + }, + Viewport { + zoom: -1., + ..viewport() + }, + Viewport { + zoom: f32::NAN, + ..viewport() + }, + Viewport { + zoom: f32::INFINITY, + ..viewport() + }, + Viewport { + pan: Vector::new(f32::NAN, 0.), + ..viewport() + }, + Viewport { + bounds: Rectangle { + width: 0., + ..viewport().bounds + }, + ..viewport() + }, + Viewport { + bounds: Rectangle { + height: -1., + ..viewport().bounds + }, + ..viewport() + }, + Viewport { + bounds: Rectangle { + x: f32::INFINITY, + ..viewport().bounds + }, + ..viewport() + }, + ] { + assert!( + Gesture::new( + saved.clone(), + id(10), + Position { x: 0., y: 0. }, + press(), + invalid + ) + .is_none() + ); + } + assert!( + Gesture::new( + saved.clone(), + id(10), + Position { x: f32::NAN, y: 0. }, + press(), + viewport() + ) + .is_none() + ); + for invalid in [Point::new(f32::NAN, 120.), Point::new(99., 120.)] { + assert!( + Gesture::new( + saved.clone(), + id(10), + Position { x: 0., y: 0. }, + invalid, + viewport() + ) + .is_none() + ); + } + } + + #[test] + fn invalid_pointer_permanently_retires_even_if_pointer_returns() { + for invalid in [Point::new(f32::NAN, 120.), Point::new(501., 120.)] { + let mut gesture = gesture(Position { x: 20., y: 40. }); + assert!(gesture.move_to(Point::new(170., 140.))); + assert!(!gesture.move_to(invalid)); + assert_eq!(gesture.delta(), Position { x: 0., y: 0. }); + assert!(!gesture.move_to(press())); + assert!(!gesture.viewport_matches(viewport())); + assert_eq!(gesture.release(press()), None); + } + assert_eq!( + gesture(Position { x: 20., y: 40. }).release(Point::new(501., 120.)), + None + ); + } + + #[test] + fn finite_inputs_that_overflow_world_geometry_retire_without_an_operation() { + let mut gesture = Gesture::new( + snapshot([Target::Placement(id(10))]), + id(10), + Position { x: 0., y: 0. }, + press(), + Viewport { + zoom: f32::MIN_POSITIVE, + ..viewport() + }, + ) + .unwrap(); + assert!(!gesture.move_to(Point::new(170., 140.))); + assert_eq!(gesture.release(press()), None); + } +} diff --git a/crates/oa-desktop/src/diagram/lifecycle_tests.rs b/crates/oa-desktop/src/diagram/lifecycle_tests.rs index cece5b70..b9ff22e2 100644 --- a/crates/oa-desktop/src/diagram/lifecycle_tests.rs +++ b/crates/oa-desktop/src/diagram/lifecycle_tests.rs @@ -53,6 +53,7 @@ fn bounds() -> Rectangle { } fn canvas(app: &App) -> DiagramCanvas<'_> { DiagramCanvas { + selection: None, project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), diff --git a/crates/oa-desktop/src/diagram/multiselect.rs b/crates/oa-desktop/src/diagram/multiselect.rs new file mode 100644 index 00000000..4b856608 --- /dev/null +++ b/crates/oa-desktop/src/diagram/multiselect.rs @@ -0,0 +1,297 @@ +use super::group_gesture::{Gesture as GroupGesture, Outcome, Viewport}; +use super::*; +use crate::app::multiselect::{Choice, Event, context}; +use crate::diagram_selection::Snapshot; +use crate::diagram_usage::Target; + +/// Even an unsupported mixed group must retain an ordinary click lifecycle. +pub(super) struct Press { + snapshot: Snapshot, + target: Target, + start: Point, + viewport: Viewport, + movement: Option, + reason: Option, + crossed: bool, + retired: bool, +} +impl Press { + fn snapshot(&self) -> &Snapshot { + self.movement + .as_ref() + .map_or(&self.snapshot, GroupGesture::snapshot) + } + fn viewport_matches(&self, viewport: Viewport) -> bool { + !self.retired + && self.viewport == viewport + && self + .movement + .as_ref() + .is_none_or(|movement| movement.viewport_matches(viewport)) + } + fn crossed_threshold(&self) -> bool { + self.movement + .as_ref() + .map_or(self.crossed, GroupGesture::crossed_threshold) + } + fn delta(&self) -> Position { + self.movement + .as_ref() + .map_or(Position { x: 0., y: 0. }, GroupGesture::delta) + } + fn move_to(&mut self, pointer: Point) { + if self.retired { + return; + } + if !pointer.x.is_finite() + || !pointer.y.is_finite() + || !self.viewport.bounds.contains(pointer) + { + self.retired = true; + self.movement = None; + return; + } + self.crossed |= pointer.distance(self.start) > 3.; + if let Some(movement) = &mut self.movement + && !movement.move_to(pointer) + { + self.retired = true; + } + } + fn release(mut self, pointer: Point) -> Option { + self.move_to(pointer); + if self.retired { + return None; + } + if !self.crossed { + return Some(Event::Collapse(self.snapshot, self.target)); + } + if let Some(reason) = self.reason { + return Some(Event::Refuse(self.snapshot, reason)); + } + match self.movement?.release(pointer)? { + Outcome::Click(id) => Some(Event::Collapse(self.snapshot, Target::Placement(id))), + Outcome::Move(snapshot, delta) => Some(Event::Move(snapshot, delta)), + } + } +} + +impl DiagramCanvas<'_> { + pub(super) fn selection_contains(&self, target: Target) -> bool { + self.selection + .is_some_and(|s| s.scope() == context(self.context).scope && s.contains(&target)) + } + + pub(super) fn group_input( + &self, + state: &mut State, + event: &canvas::Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + let viewport = Viewport { + zoom: self.zoom, + pan: self.pan, + bounds, + }; + if let Some(gesture) = &state.group { + let valid = gesture.viewport_matches(viewport) + && self.editable + && self.connection.is_none() + && !self.placing + && self + .selection + .is_some_and(|s| s.matches(gesture.snapshot(), context(self.context))); + let cancelled = matches!( + event, + canvas::Event::Window( + iced::window::Event::Unfocused | iced::window::Event::Resized(_) + ) | canvas::Event::Mouse( + mouse::Event::CursorLeft + | mouse::Event::WheelScrolled { .. } + | mouse::Event::ButtonPressed(mouse::Button::Right | mouse::Button::Middle) + ) | canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + .. + }) + ); + if !valid || cancelled { + state.group = None; + *state.cache.borrow_mut() = None; + return Some(Action::request_redraw().and_capture()); + } + } + if let Some(gesture) = &mut state.group { + match event { + canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => { + gesture.move_to(*position); + *state.cache.borrow_mut() = None; + return Some(Action::request_redraw().and_capture()); + } + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let gesture = state.group.take().unwrap(); + *state.cache.borrow_mut() = None; + let outcome = cursor + .position() + .and_then(|pointer| gesture.release(pointer)); + return Some(match outcome { + Some(event) => { + Action::publish(Message::DiagramSelection(event)).and_capture() + } + None => Action::request_redraw().and_capture(), + }); + } + canvas::Event::Keyboard(_) => return Some(Action::capture()), + _ => {} + } + } + let selection = self + .selection + .filter(|s| s.scope() == context(self.context).scope)?; + if state.pan_start.is_some() { + return None; + } + if self.connection.is_some() || self.placing { + return None; + } + if let canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key, modifiers, .. + }) = event + { + use iced::keyboard::{Key, key::Named}; + if *key == Key::Named(Named::Escape) + && state.drag.is_none() + && state.route_drag.is_none() + && state.port_drag.is_none() + && state.work.is_none() + { + return Some( + Action::publish(Message::DiagramSelection(Event::Clear(context( + self.context, + )))) + .and_capture(), + ); + } + if selection.targets().len() > 1 && *key == Key::Named(Named::Delete) { + let snapshot = selection.snapshot(context(self.context))?; + let action = if modifiers.is_empty() { + Event::Hide(snapshot) + } else { + Event::Refuse(snapshot, "Use Hide selected to remove placements. Group model-deletion review is not available yet.".into()) + }; + return Some(Action::publish(Message::DiagramSelection(action)).and_capture()); + } + } + if !matches!( + event, + canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) || !cursor.is_over(bounds) + { + return None; + } + let pointer = cursor.position()?; + let local = Point::new(pointer.x - bounds.x, pointer.y - bounds.y); + let scene = self.scene(state); + let target = if let Some(port) = self.hit_port(&scene.view, local) { + Some(Target::Port { + port: port.element, + placement: port.owner, + }) + } else if let Some(id) = self.hit(&scene.view, local) { + Some(Target::Placement(id)) + } else { + self.hit_label(&scene.routed, local) + .or_else(|| self.hit_association_label(&scene.routed, local)) + .or_else(|| self.hit_edge(&scene.routed, local).map(|hit| hit.0)) + .map(Target::Edge) + }?; + if state.modifiers.shift() { + state.drag = None; + state.preview = None; + state.port_drag = None; + state.route_drag = None; + return Some( + Action::publish(Message::DiagramSelection(Event::Choose( + context(self.context), + target, + Choice::Toggle, + ))) + .and_capture(), + ); + } + if selection.targets().len() <= 1 || !selection.contains(&target) { + return None; + } + let snapshot = selection.snapshot(context(self.context))?; + if !self.editable { + return Some(Action::capture()); + } + let reason = if crate::app::multiselect::placements(&snapshot).is_none() { + Some( + "Move requires only placed elements. Remove ports and connections from the selection.", + ) + } else if snapshot + .targets() + .iter() + .any(|t| matches!(t, Target::Placement(id) if self.work_eligible(*id))) + { + Some("Group allocation drag is not available yet. The selection was retained.") + } else { + None + }; + let movement = if reason.is_none() + && let Target::Placement(anchor) = target + { + let original = self.project.diagrams[&self.diagram].nodes[&anchor].position; + GroupGesture::new(snapshot.clone(), anchor, original, pointer, viewport) + } else { + None + }; + if movement.is_none() && reason.is_none() { + return Some(Action::capture()); + } + state.group = Some(Press { + snapshot, + target, + start: pointer, + viewport, + movement, + reason: reason.map(str::to_owned), + crossed: false, + retired: false, + }); + *state.cache.borrow_mut() = None; + Some(Action::request_redraw().and_capture()) + } + + pub(super) fn group_preview(&self, state: &State, diagram: &mut Diagram) { + let Some(gesture) = &state.group else { + return; + }; + if !gesture.crossed_threshold() { + return; + } + let Some(ids) = crate::app::multiselect::placements(gesture.snapshot()) else { + return; + }; + let Ok(Command::Batch(commands)) = oa_core::diagram_edit::translate_placements( + self.project, + self.diagram, + &ids, + gesture.delta(), + ) else { + return; + }; + // Apply only the planner's presentation records. Full model validation + // and source/history updates occur once, when App accepts the release. + for command in commands { + match command { + Command::EditNode { node, .. } => { + diagram.nodes.insert(node.id, node); + } + Command::SetEdgeRoute { route, .. } => route_edit::with_route(diagram, route), + _ => unreachable!("placement planner returned a non-geometry command"), + } + } + } +} diff --git a/crates/oa-desktop/src/diagram/multiselect_tests.rs b/crates/oa-desktop/src/diagram/multiselect_tests.rs new file mode 100644 index 00000000..0152ec1e --- /dev/null +++ b/crates/oa-desktop/src/diagram/multiselect_tests.rs @@ -0,0 +1,758 @@ +//! Production canvas event regressions for exact multi-selection and group gestures. +use super::{DiagramCanvas, State}; +use crate::app::{ + Message, + multiselect::{Choice, Event as SelectionEvent, context}, +}; +use crate::diagram_selection::Selection; +use crate::diagram_usage::Target; +use iced::widget::canvas::{self, Program}; +use iced::{Point, Rectangle, Size, Vector, event::Status, keyboard, mouse}; +use oa_core::*; + +fn position(x: f32, y: f32) -> Position { + Position { x, y } +} +fn bounds() -> Rectangle { + Rectangle::new(Point::new(260., 100.), Size::new(2400., 1800.)) +} + +struct Fixture { + project: Project, + diagram: Id, + a: Id, + b: Id, + child: Id, + internal: Id, + context: crate::route_edit::Context, +} + +impl Fixture { + fn new() -> Self { + let mut project = Project::empty(); + let a = project + .add_element(Kind::Activity, Layer::Oa, "A", None) + .unwrap(); + let b = project + .add_element(Kind::Activity, Layer::Oa, "B", None) + .unwrap(); + let c = project + .add_element(Kind::Activity, Layer::Oa, "C", None) + .unwrap(); + let child = project + .add_element(Kind::Activity, Layer::Oa, "Child", Some(a)) + .unwrap(); + let internal = project.relate(RelationKind::Interaction, "Internal", a, b); + let external = project.relate(RelationKind::Interaction, "External", b, c); + let diagram = project.ensure_diagram(Layer::Oa); + project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview; + let a = project.place(diagram, a, position(100., 200.)); + let b = project.place(diagram, b, position(500., 200.)); + let child = project.place(diagram, child, position(120., 280.)); + project.place(diagram, c, position(900., 200.)); + let view = project.diagrams.get_mut(&diagram).unwrap(); + view.nodes.get_mut(&a).unwrap().collapsed = true; + view.routes = [ + (internal, position(400., 320.)), + (external, position(800., 340.)), + ] + .into_iter() + .map(|(edge, point)| EdgeRoute { + edge, + source_attachment: None, + target_attachment: None, + waypoints: vec![point], + label: Some(EdgeLabel { + fraction: 0.4, + offset: position(3., -9.), + }), + }) + .collect(); + validate_candidate(&project).unwrap(); + Self { + project, + diagram, + a, + b, + child, + internal, + context: crate::route_edit::Context { + document: Id::new_v4(), + diagram, + revision: 7, + epoch: 11, + }, + } + } + + fn selection(&self, targets: impl IntoIterator) -> Selection { + let mut selection = Selection::new(context(self.context).scope); + for target in targets { + assert!(selection.add(target, |_| true)); + } + selection + } + + fn pair(&self) -> Selection { + self.selection([Target::Placement(self.a), Target::Placement(self.b)]) + } + + fn canvas<'a>(&'a self, selection: &'a Selection, zoom: f32) -> DiagramCanvas<'a> { + let selected = match selection.primary() { + Some(Target::Placement(id)) => self.project.diagrams[&self.diagram].nodes[id].element, + Some(Target::Edge(id)) => *id, + Some(Target::Port { port, .. }) => *port, + _ => self.diagram, + }; + DiagramCanvas { + selection: Some(selection), + project: &self.project, + view_index: None, + diagram: self.diagram, + selected, + navigation_focus: None, + zoom, + pan: Vector::new(20., 30.), + context: self.context, + selected_bend: None, + selected_attachment: None, + editable: true, + placing: false, + connection: None, + allocation: None, + } + } + + fn screen(&self, placement: Id, zoom: f32) -> Point { + let p = self.project.diagrams[&self.diagram].nodes[&placement].position; + Point::new( + bounds().x + (p.x + 30.) * zoom + 20., + bounds().y + (p.y + 25.) * zoom + 30., + ) + } + + fn expected_move(&self) -> Diagram { + // Independent literal expectations: the folded child moves with A, + // only the internal edge's authored world pin translates. + let mut expected = self.project.diagrams[&self.diagram].clone(); + expected.nodes.get_mut(&self.a).unwrap().position = position(140., 260.); + expected.nodes.get_mut(&self.b).unwrap().position = position(540., 260.); + expected.nodes.get_mut(&self.child).unwrap().position = position(160., 340.); + expected.routes[0].waypoints = vec![position(440., 380.)]; + expected + } +} + +fn input( + c: &DiagramCanvas<'_>, + state: &mut State, + event: canvas::Event, + viewport: Rectangle, + final_cursor: Point, +) -> (Option, Status) { + c.update( + state, + &event, + viewport, + mouse::Cursor::Available(final_cursor), + ) + .map_or((None, Status::Ignored), |action| { + let (message, _, status) = action.into_inner(); + (message, status) + }) +} + +fn pointer( + c: &DiagramCanvas<'_>, + state: &mut State, + event: mouse::Event, + final_cursor: Point, +) -> (Option, Status) { + input( + c, + state, + canvas::Event::Mouse(event), + bounds(), + final_cursor, + ) +} + +fn start(c: &DiagramCanvas<'_>, state: &mut State, press: Point, final_cursor: Point) { + assert!( + pointer( + c, + state, + mouse::Event::CursorMoved { position: press }, + final_cursor + ) + .0 + .is_none() + ); + let (message, status) = pointer( + c, + state, + mouse::Event::ButtonPressed(mouse::Button::Left), + final_cursor, + ); + assert!(message.is_none()); + assert_eq!(status, Status::Captured); + assert!(state.group.is_some()); + assert!(state.drag.is_none()); +} + +fn key(named: keyboard::key::Named) -> canvas::Event { + canvas::Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(named), + modified_key: keyboard::Key::Named(named), + physical_key: keyboard::key::Physical::Code(if named == keyboard::key::Named::Escape { + keyboard::key::Code::Escape + } else { + keyboard::key::Code::Delete + }), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::empty(), + text: None, + repeat: false, + }) +} + +fn expect_move( + message: Option, + selection: &Selection, + current: crate::route_edit::Context, + expected: Position, +) -> crate::diagram_selection::Snapshot { + let Some(Message::DiagramSelection(SelectionEvent::Move(snapshot, delta))) = message else { + panic!("expected one captured group Move"); + }; + assert_eq!(delta, expected); + assert!(selection.matches(&snapshot, context(current))); + snapshot +} + +#[test] +fn shift_press_uses_ordered_hit_not_native_batch_final_cursor() { + let f = Fixture::new(); + let selection = f.selection([Target::Placement(f.b)]); + let before = selection.snapshot(context(f.context)).unwrap(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let at_a = f.screen(f.a, 1.); + let final_cursor = f.screen(f.b, 1.); + assert!( + input( + &c, + &mut state, + canvas::Event::Keyboard(keyboard::Event::ModifiersChanged( + keyboard::Modifiers::SHIFT + )), + bounds(), + final_cursor + ) + .0 + .is_none() + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: at_a }, + final_cursor + ) + .0 + .is_none() + ); + let (message, status) = pointer( + &c, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + final_cursor, + ); + assert_eq!(status, Status::Captured); + assert!( + matches!(message, Some(Message::DiagramSelection(SelectionEvent::Choose(captured, Target::Placement(id), Choice::Toggle))) if captured == context(f.context) && id == f.a) + ); + assert!( + state.group.is_none() + && state.drag.is_none() + && state.route_drag.is_none() + && state.port_drag.is_none() + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + final_cursor + ) + .0 + .is_none() + ); + assert!( + selection.matches(&before, context(f.context)), + "canvas emits intent without mutating selection" + ); +} + +#[test] +fn group_canvas_preview_and_release_match_independent_expected_geometry() { + for zoom in [0.5, 1., 2.] { + let f = Fixture::new(); + let selection = f.pair(); + let captured = selection.snapshot(context(f.context)).unwrap(); + let c = f.canvas(&selection, zoom); + let mut state = State::default(); + let press = f.screen(f.a, zoom); + let end = press + Vector::new(40. * zoom, 60. * zoom); + start(&c, &mut state, press, end); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); + let (message, status) = pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end, + ); + assert!(message.is_none()); + assert_eq!(status, Status::Captured); + assert_eq!(c.preview_diagram(&state), f.expected_move()); + let (message, status) = pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ); + assert_eq!(status, Status::Captured); + let snapshot = expect_move(message, &selection, f.context, position(40., 60.)); + assert_eq!(snapshot, captured); + assert!(state.group.is_none()); + let ids = crate::app::multiselect::placements(&snapshot).unwrap(); + let command = + diagram_edit::translate_placements(&f.project, f.diagram, &ids, position(40., 60.)) + .unwrap(); + let actual = prepare_command(&f.project, &command).unwrap().project; + let mut expected = f.project.clone(); + expected.diagrams.insert(f.diagram, f.expected_move()); + assert_eq!(actual, expected); + assert!(selection.matches(&captured, context(f.context))); + } +} + +#[test] +fn stationary_group_click_does_not_use_motion_after_release_in_same_batch() { + let f = Fixture::new(); + let selection = f.pair(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(80., 70.); + start(&c, &mut state, press, end); + let (message, _) = pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ); + assert!( + matches!(message, Some(Message::DiagramSelection(SelectionEvent::Collapse(captured, Target::Placement(id)))) if selection.matches(&captured, context(f.context)) && id == f.a) + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); +} + +#[test] +fn mixed_set_keeps_click_lifecycle_and_refuses_only_actual_drag() { + for moved in [false, true] { + let f = Fixture::new(); + let selection = f.selection([Target::Placement(f.a), Target::Edge(f.internal)]); + let captured = selection.snapshot(context(f.context)).unwrap(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = if moved { + press + Vector::new(40., 60.) + } else { + press + Vector::new(2., 1.) + }; + start(&c, &mut state, press, end); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); + let (message, status) = pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ); + assert_eq!(status, Status::Captured); + if moved { + assert!( + matches!(message, Some(Message::DiagramSelection(SelectionEvent::Refuse(snapshot, reason))) if snapshot == captured && reason.contains("only placed elements")) + ); + } else { + assert!( + matches!(message, Some(Message::DiagramSelection(SelectionEvent::Collapse(current, Target::Placement(id)))) if current == captured && id == f.a) + ); + } + assert!(state.group.is_none()); + assert!(selection.matches(&captured, context(f.context))); + } +} + +#[test] +fn escape_cancels_live_group_without_clearing_or_publishing_a_model_intent() { + let f = Fixture::new(); + let selection = f.pair(); + let captured = selection.snapshot(context(f.context)).unwrap(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + start(&c, &mut state, press, end); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + assert_eq!(c.preview_diagram(&state), f.expected_move()); + let (message, status) = input( + &c, + &mut state, + key(keyboard::key::Named::Escape), + bounds(), + end, + ); + assert!(message.is_none()); + assert_eq!(status, Status::Captured); + assert!(state.group.is_none()); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); + assert!( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end + ) + .0 + .is_none() + ); + assert!(selection.matches(&captured, context(f.context))); + // A subsequent Escape, after the gesture is idle, may clear selection. + assert!(matches!( + input( + &c, + &mut state, + key(keyboard::key::Named::Escape), + bounds(), + end + ) + .0, + Some(Message::DiagramSelection(SelectionEvent::Clear(_))) + )); +} + +#[test] +fn delete_is_captured_without_hiding_during_live_group_then_release_moves_once() { + let f = Fixture::new(); + let selection = f.pair(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + start(&c, &mut state, press, end); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + let (message, status) = input( + &c, + &mut state, + key(keyboard::key::Named::Delete), + bounds(), + end, + ); + assert!(message.is_none()); + assert_eq!(status, Status::Captured); + assert!(state.group.is_some()); + assert_eq!(c.preview_diagram(&state), f.expected_move()); + expect_move( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ) + .0, + &selection, + f.context, + position(40., 60.), + ); +} + +#[test] +fn middle_pan_excludes_group_start_and_delete_even_when_left_is_pressed() { + let f = Fixture::new(); + let selection = f.pair(); + let captured = selection.snapshot(context(f.context)).unwrap(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: press }, + end + ) + .0 + .is_none() + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Middle), + end + ) + .0 + .is_none() + ); + let (message, status) = pointer( + &c, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + end, + ); + assert!(message.is_none()); + assert_eq!(status, Status::Captured); + assert!(state.group.is_none() && state.drag.is_none()); + assert!( + input( + &c, + &mut state, + key(keyboard::key::Named::Delete), + bounds(), + end + ) + .0 + .is_none() + ); + assert!( + matches!(pointer(&c, &mut state, mouse::Event::CursorMoved { position: end }, end).0, Some(Message::Pan(delta)) if delta == Vector::new(40., 60.)) + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end + ) + .0 + .is_none() + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Middle), + end + ) + .0 + .is_none() + ); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); + assert!(selection.matches(&captured, context(f.context))); +} + +#[test] +fn changed_viewport_or_context_retires_group_without_a_release_intent() { + for change in 0..7 { + let f = Fixture::new(); + let selection = f.pair(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + start(&c, &mut state, press, end); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + let mut changed = c; + let mut viewport = bounds(); + match change { + 0 => viewport.x += 1., + 1 => viewport.width -= 1., + 2 => changed.pan.x += 1., + 3 => changed.zoom = 2., + 4 => changed.context.epoch += 1, + 5 => changed.context.revision += 1, + _ => changed.context.document = Id::new_v4(), + } + let (message, _) = input( + &changed, + &mut state, + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + viewport, + end, + ); + assert!( + message.is_none(), + "changed viewport/context {change} must not publish a commit or click" + ); + assert!(state.group.is_none()); + assert_eq!( + changed.preview_diagram(&state), + f.project.diagrams[&f.diagram] + ); + } +} + +#[test] +fn changed_selection_generation_rejects_press_even_after_membership_is_restored() { + let f = Fixture::new(); + let mut selection = f.pair(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + let mut state = State::default(); + { + let c = f.canvas(&selection, 1.); + start(&c, &mut state, press, end); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + } + assert!(selection.remove(&Target::Placement(f.b))); + assert!(selection.add(Target::Placement(f.b), |_| true)); + let c = f.canvas(&selection, 1.); + let (message, status) = pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ); + assert!(message.is_none()); + assert_eq!(status, Status::Captured); + assert!(state.group.is_none()); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); +} + +#[test] +fn initial_press_without_pointer_history_consumes_release_only_displacement() { + let f = Fixture::new(); + let selection = f.pair(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + // With no earlier ordered move, the supplied release cursor is authoritative. + assert!( + pointer( + &c, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + press + ) + .0 + .is_none() + ); + assert!(state.group.is_some()); + expect_move( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ) + .0, + &selection, + f.context, + position(40., 60.), + ); +} + +#[test] +fn threshold_crossing_then_return_to_origin_emits_zero_move_not_singleton_click() { + let f = Fixture::new(); + let selection = f.pair(); + let c = f.canvas(&selection, 1.); + let mut state = State::default(); + let press = f.screen(f.a, 1.); + let end = press + Vector::new(40., 60.); + start(&c, &mut state, press, end); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: end }, + end + ) + .0 + .is_none() + ); + assert!( + pointer( + &c, + &mut state, + mouse::Event::CursorMoved { position: press }, + end + ) + .0 + .is_none() + ); + assert_eq!(c.preview_diagram(&state), f.project.diagrams[&f.diagram]); + expect_move( + pointer( + &c, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + end, + ) + .0, + &selection, + f.context, + position(0., 0.), + ); + assert_eq!(selection.targets().len(), 2); +} diff --git a/crates/oa-desktop/src/diagram/pointer_tests.rs b/crates/oa-desktop/src/diagram/pointer_tests.rs index 47d6585c..746fd5db 100644 --- a/crates/oa-desktop/src/diagram/pointer_tests.rs +++ b/crates/oa-desktop/src/diagram/pointer_tests.rs @@ -61,6 +61,7 @@ fn screen(p: Position, zoom: f32) -> Point { } fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> { DiagramCanvas { + selection: None, project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), diff --git a/crates/oa-desktop/src/diagram/port_hit_tests.rs b/crates/oa-desktop/src/diagram/port_hit_tests.rs index c1edf829..dc312fd1 100644 --- a/crates/oa-desktop/src/diagram/port_hit_tests.rs +++ b/crates/oa-desktop/src/diagram/port_hit_tests.rs @@ -2,6 +2,7 @@ use super::*; fn canvas(project: &Project, diagram: Id, zoom: f32) -> DiagramCanvas<'_> { DiagramCanvas { + selection: None, project, view_index: None, diagram, diff --git a/crates/oa-desktop/src/diagram/port_labels.rs b/crates/oa-desktop/src/diagram/port_labels.rs index 60491d26..0e6a03cb 100644 --- a/crates/oa-desktop/src/diagram/port_labels.rs +++ b/crates/oa-desktop/src/diagram/port_labels.rs @@ -177,6 +177,7 @@ mod tests { fn program(app: &App) -> PortLabels<'_> { PortLabels(DiagramCanvas { + selection: None, project: app.project(), view_index: None, diagram: app.active_diagram.unwrap(), diff --git a/crates/oa-desktop/src/diagram_selection.rs b/crates/oa-desktop/src/diagram_selection.rs new file mode 100644 index 00000000..b9201ced --- /dev/null +++ b/crates/oa-desktop/src/diagram_selection.rs @@ -0,0 +1,416 @@ +//! Transient exact-target selection; admission and model operations belong to adapters. +use crate::diagram_usage::{Surface, Target}; +use oa_core::Id; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Scope { + /// Document-instance identity, not the persisted project UUID. + pub document: Id, + pub surface: Surface, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Context { + pub scope: Scope, + pub revision: u64, + pub epoch: u64, +} + +/// Insertion-ordered unique targets. Membership does not grant edit capability. +#[derive(Debug)] +pub(crate) struct Selection { + scope: Scope, + targets: Vec, + primary: Option, + generation: u64, +} + +/// Immutable operation input, including the selection lifetime that captured it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Snapshot { + context: Context, + generation: u64, + targets: Vec, + primary: Option, +} + +impl Selection { + pub fn new(scope: Scope) -> Self { + Self { + scope, + targets: Vec::new(), + primary: None, + generation: 0, + } + } + + pub fn scope(&self) -> Scope { + self.scope + } + + pub fn targets(&self) -> &[Target] { + &self.targets + } + + pub fn primary(&self) -> Option<&Target> { + self.primary.as_ref() + } + + pub fn contains(&self, target: &Target) -> bool { + self.targets.contains(target) + } + + /// Replace with one valid target. Rejected admission preserves the old set. + pub fn replace(&mut self, target: Target, valid: impl FnOnce(&Target) -> bool) -> bool { + if !admitted(&target, valid) || (self.targets.len() == 1 && self.contains(&target)) { + return false; + } + self.targets = vec![target.clone()]; + self.primary = Some(target); + self.changed(); + true + } + + /// Add a new member as primary. Adding an existing exact target is a no-op. + pub fn add(&mut self, target: Target, valid: impl FnOnce(&Target) -> bool) -> bool { + if !admitted(&target, valid) || self.contains(&target) { + return false; + } + self.targets.push(target.clone()); + self.primary = Some(target); + self.changed(); + true + } + + /// Removal is allowed even if the target is no longer valid in the model. + pub fn remove(&mut self, target: &Target) -> bool { + let Some(index) = self.targets.iter().position(|member| member == target) else { + return false; + }; + self.targets.remove(index); + if self.primary.as_ref() == Some(target) { + self.primary = self.targets.last().cloned(); + } + self.changed(); + true + } + + pub fn toggle(&mut self, target: Target, valid: impl FnOnce(&Target) -> bool) -> bool { + if self.contains(&target) { + self.remove(&target) + } else { + self.add(target, valid) + } + } + + /// Inspect another selected member without changing membership or its order. + pub fn set_primary(&mut self, target: &Target) -> bool { + if !self.contains(target) || self.primary.as_ref() == Some(target) { + return false; + } + self.primary = Some(target.clone()); + self.changed(); + true + } + + pub fn clear(&mut self) -> bool { + if self.targets.is_empty() { + return false; + } + self.targets.clear(); + self.primary = None; + self.changed(); + true + } + + /// Change owner or prune exact targets against the current rendered surface. + /// The predicate checks identity/visibility, not eligibility for one action. + /// Revision-only changes may retain survivors; no semantic retargeting occurs. + pub fn reconcile(&mut self, scope: Scope, valid: impl FnMut(&Target) -> bool) -> bool { + if self.scope != scope { + self.scope = scope; + self.targets.clear(); + self.primary = None; + self.changed(); + return true; + } + let previous_len = self.targets.len(); + self.targets.retain(valid); + if self.targets.len() == previous_len { + return false; + } + if !self + .primary + .as_ref() + .is_some_and(|target| self.contains(target)) + { + self.primary = self.targets.last().cloned(); + } + self.changed(); + true + } + + /// Capture input, including an empty selection before a first pointer press. + /// Does not grant permission to mutate; adapters still guard drafts, modals, + /// capability and geometry. A mismatched owner cannot capture a snapshot. + pub fn snapshot(&self, context: Context) -> Option { + if context.scope != self.scope { + return None; + } + Some(Snapshot { + context, + generation: self.generation, + targets: self.targets.clone(), + primary: self.primary.clone(), + }) + } + + /// Check before any operation side effect, using the caller's live context. + /// Generation rejects remove/re-add and primary-change/restore ABA cycles. + pub fn matches(&self, snapshot: &Snapshot, current: Context) -> bool { + snapshot.context == current + && current.scope == self.scope + && snapshot.generation == self.generation + && snapshot.targets == self.targets + && self.primary == snapshot.primary + } + + fn changed(&mut self) { + self.generation = self + .generation + .checked_add(1) + .expect("selection generation exhausted"); + } +} + +impl Snapshot { + pub fn context(&self) -> Context { + self.context + } + + pub fn targets(&self) -> &[Target] { + &self.targets + } + + pub fn primary(&self) -> Option<&Target> { + self.primary.as_ref() + } +} + +fn admitted(target: &Target, valid: impl FnOnce(&Target) -> bool) -> bool { + !matches!(target, Target::Whole) && valid(target) +} + +#[cfg(test)] +mod tests { + use super::*; + use oa_core::OccurrenceEndpoint; + + fn id(value: u128) -> Id { + Id::from_u128(value) + } + + fn scope() -> Scope { + Scope { + document: id(1), + surface: Surface::Saved(id(2)), + } + } + + fn context() -> Context { + Context { + scope: scope(), + revision: 7, + epoch: 11, + } + } + + fn node(value: u128) -> Target { + Target::Placement(id(value)) + } + + #[test] + fn exact_duplicates_are_noops_but_distinct_visual_identities_survive() { + let mut selection = Selection::new(scope()); + let port_a = Target::Port { + port: id(30), + placement: id(3), + }; + let port_b = Target::Port { + port: id(30), + placement: id(4), + }; + assert!(selection.add(node(3), |_| true)); + assert!(selection.add(port_a.clone(), |_| true)); + let captured = selection.snapshot(context()).unwrap(); + assert!(!selection.add(node(3), |_| true)); + assert!(!selection.add(port_a.clone(), |_| true)); + assert!(selection.matches(&captured, context())); + assert!(selection.add(port_b.clone(), |_| true)); + assert_eq!(selection.targets(), &[node(3), port_a, port_b.clone()]); + assert_eq!(selection.primary(), Some(&port_b)); + assert_eq!( + captured.targets(), + &[ + node(3), + Target::Port { + port: id(30), + placement: id(3) + } + ] + ); + } + + #[test] + fn primary_removal_uses_latest_survivor_without_reordering() { + let mut selection = Selection::new(scope()); + for value in [3, 4, 5] { + selection.add(node(value), |_| true); + } + assert!(selection.set_primary(&node(3))); + assert_eq!(selection.targets(), &[node(3), node(4), node(5)]); + assert!(!selection.set_primary(&node(99))); + assert!(selection.remove(&node(4))); + assert_eq!(selection.primary(), Some(&node(3))); + assert!(selection.remove(&node(3))); + assert_eq!(selection.primary(), Some(&node(5))); + // A now-invalid member remains removable; validity applies to additions. + assert!(selection.toggle(node(5), |_| false)); + assert!(selection.targets().is_empty()); + assert_eq!(selection.primary(), None); + let empty = selection.snapshot(context()).unwrap(); + assert!(empty.targets().is_empty()); + assert_eq!(empty.primary(), None); + assert!(selection.matches(&empty, context())); + selection.add(node(5), |_| true); + assert!(!selection.matches(&empty, context())); + } + + #[test] + fn whole_and_rejected_targets_cannot_replace_an_existing_selection() { + let mut selection = Selection::new(scope()); + selection.add(node(3), |_| true); + let captured = selection.snapshot(context()).unwrap(); + assert!(!selection.replace(Target::Whole, |_| true)); + assert!(!selection.add(Target::Whole, |_| true)); + assert!(!selection.toggle(Target::Whole, |_| true)); + assert!(!selection.replace(node(4), |_| false)); + assert!(!selection.toggle(node(4), |_| false)); + assert!(selection.matches(&captured, context())); + assert!(selection.replace(node(4), |_| true)); + assert_eq!(selection.targets(), &[node(4)]); + assert_eq!(selection.primary(), Some(&node(4))); + assert!(!selection.replace(node(4), |_| true)); + assert!(selection.clear()); + assert!(!selection.clear()); + assert_eq!(selection.primary(), None); + } + + #[test] + fn reused_process_occurrences_reconcile_by_full_path_never_definition() { + let owner = Scope { + surface: Surface::Process(id(2)), + ..scope() + }; + let mut selection = Selection::new(owner); + let occurrence = |path| { + Target::ProcessOccurrence(OccurrenceEndpoint { + references: vec![id(path), id(50)], + occurrence: id(60), + }) + }; + let first = occurrence(10); + let second = occurrence(20); + selection.add(first.clone(), |_| true); + selection.add(second.clone(), |_| true); + assert_eq!(selection.targets(), &[first.clone(), second.clone()]); + assert!(selection.reconcile(owner, |target| target == &first)); + assert_eq!(selection.targets(), std::slice::from_ref(&first)); + assert_eq!(selection.primary(), Some(&first)); + // A new call path with the same definition is not a replacement identity. + let replacement = occurrence(70); + assert!(selection.reconcile(owner, |target| target == &replacement)); + assert!(selection.targets().is_empty()); + assert_eq!(selection.primary(), None); + } + + #[test] + fn revision_reconciliation_retains_exact_survivors_but_invalidates_operations() { + let mut selection = Selection::new(scope()); + selection.add(node(3), |_| true); + selection.add(node(4), |_| true); + selection.set_primary(&node(3)); + let captured = selection.snapshot(context()).unwrap(); + let next = Context { + revision: 8, + ..context() + }; + assert!(!selection.reconcile(scope(), |_| true)); + assert_eq!(selection.targets(), &[node(3), node(4)]); + assert_eq!(selection.primary(), Some(&node(3))); + assert!(!selection.matches(&captured, next)); + let accepted = selection.snapshot(next).unwrap(); + assert!(selection.matches(&accepted, next)); + assert!(selection.reconcile(scope(), |target| target != &node(4))); + assert_eq!(selection.primary(), Some(&node(3))); + assert!(!selection.matches(&accepted, next)); + } + + #[test] + fn changing_document_or_surface_clears_even_identical_target_ids() { + for changed in [ + Scope { + document: id(99), + ..scope() + }, + Scope { + surface: Surface::Saved(id(99)), + ..scope() + }, + Scope { + surface: Surface::Scenario(id(2)), + ..scope() + }, + ] { + let mut selection = Selection::new(scope()); + selection.add(node(3), |_| true); + let captured = selection.snapshot(context()).unwrap(); + assert!(selection.reconcile(changed, |_| true)); + assert_eq!(selection.scope(), changed); + assert!(selection.targets().is_empty()); + assert_eq!(selection.primary(), None); + assert!(!selection.matches(&captured, context())); + assert!(selection.snapshot(context()).is_none()); + selection.reconcile(scope(), |_| true); + selection.add(node(3), |_| true); + assert!(!selection.matches(&captured, context())); + } + } + + #[test] + fn stale_epoch_selection_and_primary_cycles_do_not_regain_authority() { + let mut selection = Selection::new(scope()); + selection.add(node(3), |_| true); + selection.add(node(4), |_| true); + let captured = selection.snapshot(context()).unwrap(); + assert_eq!(captured.context(), context()); + assert_eq!(captured.primary(), Some(&node(4))); + assert!(!selection.matches( + &captured, + Context { + epoch: 12, + ..context() + } + )); + selection.set_primary(&node(3)); + selection.set_primary(&node(4)); + assert!(!selection.matches(&captured, context())); + let restored_primary = selection.snapshot(context()).unwrap(); + selection.remove(&node(4)); + selection.add(node(4), |_| true); + assert_eq!(selection.targets(), captured.targets()); + assert_eq!(selection.primary(), captured.primary()); + assert!(!selection.matches(&restored_primary, context())); + assert!(selection.matches(&selection.snapshot(context()).unwrap(), context())); + } +} diff --git a/crates/oa-desktop/src/main.rs b/crates/oa-desktop/src/main.rs index 02bf1b9e..188a21f2 100644 --- a/crates/oa-desktop/src/main.rs +++ b/crates/oa-desktop/src/main.rs @@ -10,6 +10,7 @@ mod data_values; mod data_view; mod diagnostic_input; mod diagram; +mod diagram_selection; mod diagram_usage; mod document; mod explorer; diff --git a/crates/oa-desktop/src/model_menu.rs b/crates/oa-desktop/src/model_menu.rs index c0a4cbf8..4ca4b6a0 100644 --- a/crates/oa-desktop/src/model_menu.rs +++ b/crates/oa-desktop/src/model_menu.rs @@ -12,6 +12,7 @@ pub struct Request { pub anchor: Point, pub object: Id, pub placement: Option, + pub selection_target: Option>, pub bend: Option, pub route_point: Option, pub route_segment: Option<(usize, Position)>, @@ -25,6 +26,7 @@ impl Request { anchor, object, placement: None, + selection_target: None, bend: None, route_point: None, route_segment: None, @@ -55,6 +57,7 @@ pub struct Opened { impl Opened { pub fn new(app: &App, request: Request, token: u64) -> Option { if !app.project().contains(request.object) + || !target_matches(app, &request) || request .canvas .is_some_and(|c| Some(c) != current_canvas(app)) @@ -80,6 +83,7 @@ impl Opened { && self.layer == app.layer && self.diagram == current_diagram(app) && app.project().contains(self.request.object) + && target_matches(app, &self.request) && self .request .canvas @@ -92,6 +96,32 @@ impl Opened { } } +fn target_matches(app: &App, request: &Request) -> bool { + use crate::diagram_usage::Target; + let Some(target) = request.selection_target.as_deref() else { + return true; + }; + if request.canvas.is_none() || !app.multiselect_target_visible(target) { + return false; + } + match target { + Target::Placement(id) => { + request.placement == Some(*id) + && request + .canvas + .and_then(|context| app.project().diagrams.get(&context.diagram)) + .and_then(|diagram| diagram.nodes.get(id)) + .is_some_and(|node| node.element == request.object || node.id == request.object) + } + Target::Port { port, .. } => request.placement.is_none() && *port == request.object, + Target::Edge(id) => request.placement.is_none() && *id == request.object, + Target::StateRow { relation, .. } => { + request.placement.is_none() && *relation == request.object + } + _ => false, + } +} + fn current_canvas(app: &App) -> Option { if app.page == Page::Behavior { app.state_context() @@ -126,7 +156,22 @@ pub fn trigger(app: &App) -> iced::Element<'_, Message> { request.canvas = app.route_context(); } else if app.page == Page::Behavior { request.canvas = app.state_context(); - if let Some(context) = request.canvas + 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() @@ -187,12 +232,112 @@ pub fn popup(app: &App) -> Option> { on_close: Message::Menu(Event::Close), }); } - if app.page == Page::Behavior - && let Some(menu) = crate::behavior::state_menu::popup(app, opened, clean) + let mut entries = vec![Entry::title(crate::views::short(p.named(id), 38))]; + if request.canvas.is_some() + && let Some(context) = app.multiselect_context() { + use crate::app::multiselect::{Choice, Event as SelectionEvent}; + use crate::diagram_usage::Target; + let view = request + .canvas + .map(|context| context.diagram) + .map(|d| projection::project_view_with_index(p, d, app.document.view_index())); + let target = request + .selection_target + .as_deref() + .cloned() + .or_else(|| request.placement.map(Target::Placement)) + .or_else(|| { + view.as_ref()? + .ports + .iter() + .find(|port| port.element == id) + .map(|port| Target::Port { + port: port.element, + placement: port.owner, + }) + }) + .or_else(|| { + view.as_ref()? + .edges + .iter() + .any(|edge| edge.id == id) + .then_some(Target::Edge(id)) + }); + if let Some(target) = target { + let selected = app + .diagram_selection + .as_ref() + .is_some_and(|s| s.contains(&target)); + entries.push(item( + if selected { + "Remove from selection" + } else { + "Add to selection" + }, + Message::DiagramSelection(SelectionEvent::Choose( + context, + target.clone(), + if selected { + Choice::Remove + } else { + Choice::Add + }, + )), + true, + )); + if let Some(snapshot) = app.multiselect_snapshot().filter(|s| s.targets().len() > 1) + && selected + { + entries.push(Entry::title(format!( + "{} selected", + snapshot.targets().len() + ))); + entries.push(item( + "Inspect this element", + Message::DiagramSelection(SelectionEvent::Choose( + context, + target.clone(), + Choice::Inspect, + )), + true, + )); + entries.push(item( + "Select only this element", + Message::DiagramSelection(SelectionEvent::Choose( + context, + target, + Choice::Replace, + )), + true, + )); + entries.push(item( + "Hide selected placements", + Message::DiagramSelection(SelectionEvent::Hide(snapshot.clone())), + crate::app::multiselect::placements(&snapshot).is_some(), + )); + entries.push(Entry::disabled( + "Delete selected from model (not yet available)", + )); + return Some(Menu { + token: opened.token, + anchor: request.anchor, + entries, + on_close: Message::Menu(Event::Close), + }); + } + entries.push(Entry::separator()); + } + } + if app.page == Page::Behavior + && let Some(mut menu) = crate::behavior::state_menu::popup(app, opened, clean) + { + if entries.len() > 1 { + entries.extend(menu.entries.into_iter().skip(1)); + menu.entries = entries; + } return Some(menu); } - let mut entries = vec![Entry::title(crate::views::short(p.named(id), 38))]; if !clean { entries.push(Entry::title("Apply or revert pending edits first")); } diff --git a/crates/oa-desktop/src/views.rs b/crates/oa-desktop/src/views.rs index 3f53c435..1f570f6e 100644 --- a/crates/oa-desktop/src/views.rs +++ b/crates/oa-desktop/src/views.rs @@ -468,6 +468,7 @@ fn diagram(app: &App) -> View<'_, Message> { .align_y(iced::Center); let canvas: View<'_, Message> = if let Some(id) = app.active_diagram { let program = DiagramCanvas { + selection: app.diagram_selection.as_ref(), allocation: Some(app.allocation_capability()), project: app.project(), view_index: Some(app.document.view_index()), @@ -621,6 +622,37 @@ fn diagram(app: &App) -> View<'_, Message> { ) .is_some_and(|(source, target)| source != edge.source || target != edge.target) }); + let selection = app.multiselect_snapshot(); + let count = selection.as_ref().map_or(0, |s| s.targets().len()); + let mut selection_controls = + row![label(format!("{count} selected")).wrapping(widget::text::Wrapping::None)] + .spacing(7) + .align_y(iced::Center) + .height(32); + if let Some(snapshot) = selection.as_ref().filter(|s| s.targets().len() > 1) { + if crate::app::multiselect::placements(snapshot).is_some() { + selection_controls = selection_controls.push(action( + "Hide selected", + Message::DiagramSelection(crate::app::multiselect::Event::Hide( + snapshot.clone(), + )), + )); + } + selection_controls = selection_controls + .push(action( + "Clear selection", + Message::DiagramSelection(crate::app::multiselect::Event::Clear( + snapshot.context(), + )), + )) + .push( + label(format!( + "Properties edit only {}", + short(app.project().named(app.selected), 36) + )) + .wrapping(widget::text::Wrapping::None), + ); + } let hint = if let Some(feedback) = &app.allocation_drag.feedback { feedback.text.as_str() } else if let Some(session) = &app.connection { @@ -633,16 +665,37 @@ fn diagram(app: &App) -> View<'_, Message> { } else { session.tool.hint() } + } else if count > 1 { + if selection + .as_ref() + .is_some_and(|s| crate::app::multiselect::placements(s).is_some()) + && projection != Some(ProjectionKind::Allocation) + { + "Shift-click to add/remove · drag selected to move · right-click for actions · scroll to zoom" + } else { + "Shift-click to add/remove · right-click for selection actions · Esc clears selection" + } } else if promoted { "Expand endpoint containers to edit this connection route" } else if edge.is_some() { "Right-click for routing · drag line / bend / label · Insert adds a bend · Esc cancels" } else { - "Right-click for actions · drag to arrange · scroll to zoom" + "Shift-click to add/remove · right-click for actions · drag to arrange · scroll to zoom" }; - // Status is outside the measured canvas. Clipping prevents long text - // from painting over work headers or the dedicated destination slot. - tools = tools.push(container(label(hint)).height(30).width(Fill).clip(true)); + // Selection changes must not resize the measured canvas or refit its + // camera. Reserve both rows even when no group actions are available. + tools = tools.push( + container( + column![ + selection_controls, + label(hint).wrapping(widget::text::Wrapping::None) + ] + .spacing(3), + ) + .height(56) + .width(Fill) + .clip(true), + ); } column![container(tools).padding(14), canvas].into() } diff --git a/harness/DECISIONS.md b/harness/DECISIONS.md index 498cbce8..40c051ef 100644 --- a/harness/DECISIONS.md +++ b/harness/DECISIONS.md @@ -489,3 +489,12 @@ Missing global and scoped parameter IDs remain explicit stabilization metadata, not new random identities assigned on each parse. Public source8 integration must thread the same accounting through the outer project and included files; this private checkpoint does not itself admit a complete version8 source bundle. + +## Connection-end presentation preferences — 2026-09-22 + +Issue #33 uses optional side/fraction attachments on each view's `EdgeRoute`, +with semantic Port precedence and shared shape-aware routing. Visual attachment +editing remains separate from semantic reconnect. The [decision and evidence +map](edge-attachments-20260922.md) records alternatives, lifecycle rules, +independent falsifiers, evaluators and the Process/Scenario applicability limits. +This is branch-specific work; it does not establish an integrated release. diff --git a/harness/edge-attachments-20260922.md b/harness/edge-attachments-20260922.md new file mode 100644 index 00000000..3ce9abf5 --- /dev/null +++ b/harness/edge-attachments-20260922.md @@ -0,0 +1,103 @@ +# Connection attachment editing — issue 33 + +Branch-specific work from `3f9e6a18af96b5cd10940da4093be8da28b9c835` on +`codex/edge-anchors-ytr83r-20260922`, owned by Codex-ytr83r/root. +[Live scope and handoff](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/33) +remain in Gitea. This is not an integrated release checkpoint. + +The user needs to move where a connection touches an element without changing +the relationship. The existing ordinary route editor offered bends and labels; +unported ends were selected from four side midpoints. SA semantic ports already +had per-placement anchors. State endpoint handles instead performed semantic +reconnect. These are different operations and remain distinct. + +## Decision and alternatives + +Store optional `source_attachment` and `target_attachment` values on `EdgeRoute`. +Each contains a side and normalized fraction, interpreted against the actual +projected shape. No additional semantic object, ID or reference is introduced. +The precise format, geometry, Port precedence, folding, reconnect and reset rules +are in [the normative language contract](../docs/language.rst); REST uses the +same data and existing `SetEdgeRoute` transaction. + +Compared alternatives: + +- Absolute endpoint coordinates are easy to store but become detached from the + owner after movement/resizing and duplicate derived geometry. +- Reusing semantic Port anchors would invent ports for OA/state relationships + and conflate each connection's presentation with a shared port's identity. +- Binding an attachment to an additional semantic endpoint/placement reference + would preserve the old owner's identity after reconnect, but needs stale + reference cleanup and a dormant preference lifecycle without a current user + need. The selected rule retains the edge-end preference across reconnect, + just as authored bends are retained; it is documented, not inferred silently. + +Fractions remain precise in source/API; pointer edits snap to hundredths. +Automatic absence and authored values are distinct. Press/release without a +drag, Escape, stale context, focus/camera/layout change and outside release do +not author a preference. Returning within three screen pixels of the press +restores the exact initial optional value, including its original precision. +Successful editing changes one field in one route in one view in one history +operation. Per-end reset preserves the other end, bends and label. + +Ordinary diagrams expose eligible selected-end handles and explicit source/target +menu choice for overlapping handles. State menus expose separately labelled +Adjust attachment and Reconnect actions; selecting a transition still retains +the existing reconnect gesture. Ports keep their existing movement policy. +This work does not adopt or alter the separate #16 resize/port-capacity policy. + +## Review-driven corrections and evaluators + +Independent contract review identified projection-filtered Port markers as an +unsafe authority: eligibility and routing now consult semantic endpoint identity. +Attachment metadata participates in the route-preview cache key. Inset contours +use actual lead lengths for path costs and pin fractions; shape scan-line +intersections are shared by routing and pointer gestures. Side choice uses the +nearest cardinal bounding segment, avoiding coincident circle scan-line points +that otherwise retain the wrong exit direction. Hysteresis is capped by symbol +size so small circles can change sides at low zoom; an explicit regression +covers midpoint, overshoot and near-corner intent. + +The first frozen gate exposed a remaining overshoot case: distances measured +from far outside a small symbol converged, keeping the previous perpendicular +side. Measuring side intent at the cursor clamped to the bounding box preserves +the intended side under overshoot. The failed gate is retained separately. +Valid Port-to-Port Item traces also omit projected port IDs; routing now resolves +the semantic port before looking up its visible, owner-qualified marker. An +actual trace regression checks moved anchors and unchanged trace metadata. + +Coincident authored self ends previously threatened an empty candidate list. +They retain their positions and receive an explicit loop warning. A critic then +showed a fixed invented detour point could collide with a neighboring card even +though the opposite side was clear. Computed detours now consider both directions +and multiple bounded extents, preferring clear candidates. These points never +become persisted hard pins. Geometry tests exercise blockers on either side. + +Cross-review found ordinary attachment drags survived camera/focus/layout +changes and outside release. They now carry a viewport stamp and cancel on those +changes, with an actual pointer-event cancellation matrix. The same review +reconciled return-to-press behavior across ordinary and State canvases. + +Meaningful evaluators are `oa-core` integration tests `attachment_geometry`, +`attachment_lifecycle`, `attachment_trace_ports`, `attachment_workshop`, +`edge_attachments` and the existing routing/port/state suites; desktop ordinary +attachment and State pointer/planner tests; server `http::edge_attachments` +conditional transactions, exact source exports and restart. Tests compare complete +projects/source where applicable, not just attachment fields. Native acceptance +uses the [fixture and workflow plan](experiments/edge-attachments-20260922/native-plan.md). +Final test/native results and exact tested commit belong in the publication +handoff, not an unverified claim in this decision record. + +## Applicability and integration limits + +Process topology uses generated cards and qualified contextual addresses. +Scenario endpoints derive from lifelines, activations, lifecycle and time/order +notation. Neither renderer consumes ordinary `Diagram.routes`; their visual +editing requires separate notation-aware contracts. This change does not claim +to implement arbitrary attachment dragging there, or to resolve automatic +arrangement (#32) or State bend editing (#23). + +The base includes ordinary Text work, not the separately published Scenario +spacing/inspector branches or the latest transition lifecycle branch. Integration +owner #27 must compose actual pushed commits and verify the combined result. +Linux/WSL verification remains paused by the user. diff --git a/harness/experiments/edge-attachments-20260922/native-plan.md b/harness/experiments/edge-attachments-20260922/native-plan.md new file mode 100644 index 00000000..9e652665 --- /dev/null +++ b/harness/experiments/edge-attachments-20260922/native-plan.md @@ -0,0 +1,70 @@ +# Native endpoint attachment acceptance + +This is a prepared plan, not a record of passed native checks. `native-fixture.arcadia` is a deliberately small schema-7 source fixture with 49 explicit stable UUIDs. The parent must parse and validate it with the candidate before staging a working copy. No GUI, compilation, or evaluator has been run for this preparation. + +## Bind the run + +Record the exact candidate commit, source/worktree cleanliness, GUI/runtime hashes, successful gate receipts, fixture hash and recipe hash. Keep an immutable baseline and a separate mutable working source; use an isolated profile. Record launch, normal close, stdout/stderr, saved checkpoints, and captures. Compile baseline and checkpoints to full typed Project values with the same pinned source evaluator. Use a baseline-derived expected Project with only the named field changes; compare the entire Project, not a JSON subset or screenshot. Source text formatting may differ after save; source must parse to the exact expected Project. Save/reopen comparisons must use the same expected Project. Preserve raw files and integer precision. + +The fixture contains two OA activities and their one interaction, one small SA state machine, and a separate two-function semantic-port regression view. Its controller is semantic ownership, not a large canvas placement. The State view places the machine, region, and all five vertices. Two saved views per edited graph expose view-isolation failures. View 05 is separate to keep the main workflows uncluttered. + +## Identities and geometry + +Every ID has prefix `ea202922-0033-4000-8000-`; the twelve-digit suffix below is decimal-padded. The source file is authoritative for all fields. + +| Object / alias | ID suffix | Purpose | +|---|---:|---| +| `dispatch_request` | 20 | Stored source `receive_report` (10), target `dispatch_crew` (11) | +| `dispatch_view` / `dispatch_reference` | 100 / 101 | Edited OA view / untouched OA view | +| `readiness` / `main_region` | 31 / 32 | Machine / region membership | +| `start`, `ready`, `active`, `decision`, `finished` | 33–37 | Initial, two stable states, choice, final | +| `activate` | 41 | Stored Ready source → Active target | +| `initialize` / `finish` | 40 / 44 | Circle-source / diamond-source contour checks | +| `refresh` / `internal_check` | 45 / 46 | External self-transition / internal row | +| `readiness_view` / `readiness_reference` | 200 / 201 | Edited State view / untouched State view | +| `status_exchange` / `ports_view` | 54 / 300 | Real semantic ports regression | + +Coordinates below are **diagram coordinates**, not assumed native screen coordinates. After Fit, identify bounds and zoom/pan from actual UI; map `window = canvas origin + pan + zoom × diagram`. State canvas includes a guidance strip in its offset. Confirm painted handles before pressing; retain screenshots documenting the mapping. Source and target describe stored ends, regardless of arrow appearance. + +| View / endpoint | Baseline attachment / contour point | Intended destination | +|---|---|---| +| OA `dispatch_request` source | Right 0.5 → (340,240) | Top 0.25 → (160,180) | +| OA `dispatch_request` target | Left 0.6 → (600,252) | Bottom 0.75 → (780,300) | +| State `activate` source | Right 0.5 → (430,270) | Top 0.25 → (250,210) | +| State `activate` target | Left 0.6 → (570,294) | Bottom 0.75 → (750,350) | +| State `initialize` initial source | Right 0.5 → (126,260) | Top 0.5 → (118,252) | +| State `finish` choice source | Right 0.5 → (698,444) | Top 0.5 → (684,430) | +| Real `status_out` / `status_in` | Right 0.5 → (340,240) / Left 0.5 → (600,240) | No attachment editing permitted | + +For rounded corners, the quarter-width points are on the straight section. Circle and diamond checks intentionally use contour extrema with unambiguous 0.5 offsets. Native drag input may quantize to adjacent hundredths: if the intended fraction is not attained, do not change the expected oracle to match output. Repeat from the prior checkpoint using the painted geometry, or record the observed limitation and narrow the claim. + +## OA workflow and exact expectations + +1. Open view **01 Dispatch — edit attachments** and Fit. Select `Dispatch request` on its line. Confirm distinct endpoint handles and context actions. Save baseline checkpoint `00` before edits; full Project equals baseline B. +2. Drag source handle from (340,240) to (160,180), inspect live preview and release. Save `01`. Expected O1 = B with **only** `diagrams[100].routes[edge20].source_attachment = {Top,0.25}`. Target sentinel `{Left,0.6}`, waypoint `(440,340)`, label `{fraction:0.5,offset:(12,-18)}`, relation description/direction, nodes and all other views remain exact. +3. Undo and save `02`: B exactly. Redo and save `03`: O1 exactly. One Undo must remove the entire drag, not a partial movement. +4. Drag target to (780,300), save `04`: O2 = O1 with only target `{Bottom,0.75}`. Open source and target context actions to ensure end identity is clear. Use **Reset source attachment**, save `05`: O3 = O2 with source None; preserve target, waypoint and label. Undo returns O2, redo returns O3. +5. Start a drag, move away and return within three screen pixels of the press, then release: no source/history change. Repeat a drag canceled with Escape; repeat with right click, middle pan, wheel/camera change, focus loss, and release outside. Save a checkpoint after each meaningful cancellation; expect O3 exactly. Camera-only state is outside Project. After these checks, one Undo still reverses the last real reset (O2), showing no hidden history entry; Redo restores O3. +6. Open view **02 Dispatch — untouched reference**: its independent target `{Bottom,0.25}` and placements remain baseline. Return to 01, save, close normally, reopen the saved working file and save `06`: O3 exactly. Confirm the saved source contains the attachment and existing waypoint/label. + +## State workflow and exact expectations + +Continue from saved O3 so the State trial also proves preservation of the prior OA edits. The frozen expected catalog composes State changes on O3; B remains the unchanged comparison for view 201. + +1. Open **Rescue readiness** in Behavior / State diagram and choose **03 Readiness — edit attachments**. Check source-defined membership is visible, with Ready and Active readable and no nested hierarchy. Select `Activate`; choose **Adjust source attachment** from its actions, then drag the highlighted source to (250,210). Save `S01`: S1 = O3 with only `diagrams[200].routes[edge41].source_attachment={Top,0.25}`. Preserve target `{Left,0.6}`, waypoint `(500,310)`, label `{fraction:0.5,offset:(8,-16)}`, all semantic transitions and view 201. +2. Undo → O3, redo → S1. Adjust target to (750,350), save `S02`: S2 adds only target `{Bottom,0.75}`. Reset target and save `S03`: S3 has target None, preserves source/bend/label. Cancel an attachment gesture with Escape and perform away-return and wheel interruption; checkpoints remain S3. View **04 Readiness — untouched reference** stays B exactly. +3. Choose `Initialize` → **Adjust source attachment**, drag the initial circle source to (118,252), save `S04`: S4 = S3 with only route edge40 source `{Top,0.5}`. Choose `Finish` → Adjust source, drag the choice source to (684,430), save `S05`: S5 = S4 with only route edge44 source `{Top,0.5}`. Capture both painted contours; the typed checkpoint is the persistence oracle. Undo/redo each as practical. Save/normal-close/reopen final checkpoint: S5 exactly. +4. Inspect external `Refresh: Active → Active`: both Adjust source and Adjust target must be discoverable and identify the corresponding end even on a self-transition. Inspect `Internal status check` row: it must not offer external attachment handles/actions. Do not infer every symbol was exercised from these checks. +5. **Separate reversible reconnect trial from S5:** with Select / move, drag the selected `Activate` target handle from Active to `Dispatch decision` (or use explicitly labelled Reconnect target). Expected R1 differs from S5 only in relation41's semantic target: 35 → 36. Route presentation fields and every other relation field remain unchanged. Undo must restore S5. This distinguishes semantic reconnect from Adjust; do not mix its checkpoint with S5. If cardinality/validity blocks the trial, record it and do not claim reconnect success without a valid alternative approved against source rules. + +## Real semantic ports regression + +Open **05 Status — semantic ports regression**. Select the `Readiness status` functional exchange. Both attachment Move/Reset actions must be disabled or absent because source_port52 and target_port53 remain authoritative. Drag Status output to its owner's Top/0.25 position through the established port-layout interaction, then Undo. That trial may change only the emitting node's port_anchor and must restore S5 exactly on Undo; relation/source_port/target_port identities must never change. No edge-attachment record should be created by port dragging. Record UI hit selection and full saved Project, then leave this trial out of the final OA/State checkpoint. + +## Acceptance and limits + +Require exact full-Project checks for every named checkpoint, normal-close/reopen retention, source/target discoverability, useful previews, and cancellation without hidden history. Inspect native fit/readability and any port/label/handle overlap; reduce claim or fix defects rather than interpreting screenshots as model proof. This fixture does not establish Process/Scenario authoring support, all state symbols, every projection, promoted/folded endpoints, continuous held-motion performance, or exact camera persistence. Those require separate evidence; targeted automated tests remain complementary. Runtime source parsing/validation and native execution are pending parent gates. + +### Prepared layout clearance correction + +The edited State view places Ready and Active at y=210 and Startup at y=252. The reference view intentionally retains its earlier positions. Region header clearance reaches y=180 (104 + 64 + 12); the planned Ready top lead now ends at y=192 (210 - 18), above the card but below the header obstacle. This changes only the three edited-view placements; the manual waypoint (500,310), both route labels, semantic objects, and reference view are preserved. `crates/oa-core/tests/attachment_workshop.rs` validates the fixture and routes the named edited edges at every planned checkpoint. Test execution is pending the parent gate; no clear-path claim is made from static inspection alone. diff --git a/harness/experiments/multiselect-20260923/.gitattributes b/harness/experiments/multiselect-20260923/.gitattributes new file mode 100644 index 00000000..dea3bb8a --- /dev/null +++ b/harness/experiments/multiselect-20260923/.gitattributes @@ -0,0 +1,3 @@ +# Preserve the exact pinned evidence and verifier bytes through Git round trips. +* -text whitespace=cr-at-eol +incidents/**/*.arcadia whitespace=-blank-at-eof,cr-at-eol diff --git a/harness/experiments/multiselect-20260923/checks/README.md b/harness/experiments/multiselect-20260923/checks/README.md new file mode 100644 index 00000000..77704824 --- /dev/null +++ b/harness/experiments/multiselect-20260923/checks/README.md @@ -0,0 +1,96 @@ +# Closed multiselect gate evidence + +This package preserves all 19 closed captures selected from +`.local/group-movement-gates`: 14 recorded exit 0, and five recorded exit 101. +Every run reports unchanged captured inputs. The five failures remain failures; +their complete original receipts and streams are included. See the separate +[saved-view report](../../../multiselect-saved-view-20260923.md) for scope and +native observations. These standard-library Python 3.10+ tools do not run Cargo, +Git, archived source or executables. + +| Run | Recorded exit | Input files | +| --- | ---: | ---: | +| core-1 | 0 | 722 | +| core-2 | 101 | 729 | +| core-3 | 0 | 729 | +| desktop-1 | 101 | 727 | +| desktop-2 | 101 | 728 | +| desktop-3 | 0 | 729 | +| desktop-4 | 0 | 729 | +| desktop-5 | 0 | 729 | +| format-1 | 0 | 729 | +| format-2 | 0 | 729 | +| lint-1 | 101 | 729 | +| lint-2 | 0 | 729 | +| lint-3 | 0 | 729 | +| native-build-1 | 0 | 729 | +| native-build-2 | 0 | 729 | +| oracle-2 | 101 | 729 | +| oracle-3 | 0 | 729 | +| windows-build-1 | 0 | 729 | +| windows-build-2 | 0 | 729 | + +Core-3, desktop-4, format-1, lint-2, native-build-1 and windows-build-1 share the +729-file map `55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb`. +Desktop-5, format-2, lint-3, native-build-2 and windows-build-2 share the distinct +729-file map `99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c`. +The [source identity verifier](../source/README.md) compares those maps with Git +commits `12a7352ddef5f69a144ead8219a50916233d63a9` and +`f6a6010baed9e6164a1fcc7ecd58256a562827d7`, respectively. Earlier maps retain +their own evidence scope; they are not reruns against the later source. + +To validate this exact raw run list without writing a package, from the checkout +root run: + +```text +python -B harness/experiments/multiselect-20260923/checks/prepare.py --root . --runs core-1 core-2 core-3 desktop-1 desktop-2 desktop-3 desktop-4 desktop-5 format-1 format-2 lint-1 lint-2 lint-3 native-build-1 native-build-2 oracle-2 oracle-3 windows-build-1 windows-build-2 --check-only +``` + +Missing closure, changed inputs, inconsistent receipts, unavailable exact helpers, +and extra/unexpected run files are refused. Run names must be supplied, must be +unique, and cannot contain path separators. + +To create a package, use the same explicit `--runs` list, omit `--check-only`, and +optionally supply `--output PATH`. The default output is this checks directory. +An output package is not overwritten. `--helper PATH` can be repeated to supply +an older gate helper whose exact pinned bytes are no longer the current helper. +Default helper candidates are `.local/group-movement-gates/*.py`. + +Verify a completed package anywhere, without the original checkout: + +```text +python -B PATH_TO_PACKAGE/verify.py +``` + +The packager preserves the seven original files per run: started/process/result +receipts, stdout/stderr logs, the input map, and its source ZIP. It preserves the +exact helper bytes named by each receipt. Only byte-identical input maps and ZIPs +are deduplicated into hash-named capsules; their per-run logical mapping remains. +ZIP member ordering and timestamps are fixed. Raw input/log bytes are unchanged. + +`manifest.json` records the explicit run list, outcomes, commands, recorded HEADs, +input counts, helper paths, logical mapping and every stored member pin. +`files.json` pins `prepare.py`, `verify.py`, this README, the manifest and archive. +The verifier checks safe exact ZIP membership (including nested source ZIPs), +every recorded byte count/hash, input counts, the 71 vendored iced_widget files, +start/process/finish order, receipt relationships, unchanged-input flags, helper +identity, and exact ordered executable-artifact records parsed from Cargo JSON +stdout. Missing JSON artifact records remain missing; they are not synthesized +from paths or inferred from a successful exit. Non-null artifact readback pins +are checked structurally; executable bytes are not included or independently +verified by this package. + +The source capsules are **local historical input snapshots**, including captured +uncommitted work. A recorded HEAD alone does not identify their contents. They +are not an integrated release, a hermetic toolchain distribution, or a claim that +one run's gate applies to a later input map. Commands, paths, PIDs, times and +artifact hashes are historical receipts, not authenticated execution records. +The verifier checks their internal consistency and does not access historical +absolute paths, contact services, extract archives or rerun tests. Pin integrity +is not an external signature or proof against coordinated receipt alteration. + +This package has no native interaction, focus, selection, movement, saved-model, +Undo/Redo or full #29 acceptance implication. The native evidence and its +independent evaluator belong in their separate reviewed record. The verification +summary reports captured outcomes, including failures; it does not declare the +feature or all captured gates successful. diff --git a/harness/experiments/multiselect-20260923/checks/evidence.zip b/harness/experiments/multiselect-20260923/checks/evidence.zip new file mode 100644 index 00000000..b10d467f Binary files /dev/null and b/harness/experiments/multiselect-20260923/checks/evidence.zip differ diff --git a/harness/experiments/multiselect-20260923/checks/files.json b/harness/experiments/multiselect-20260923/checks/files.json new file mode 100644 index 00000000..0d5c0f04 --- /dev/null +++ b/harness/experiments/multiselect-20260923/checks/files.json @@ -0,0 +1,22 @@ +{ + "prepare.py": { + "bytes": 4818, + "sha256": "29eedcec091874b26572530517f015ab690ed1a9040abea52248e99603ba5f25" + }, + "verify.py": { + "bytes": 8873, + "sha256": "c03513407073bef092d57b968f982b8ffc0c58e095f834fe7464c6a0e8454a9a" + }, + "README.md": { + "bytes": 5001, + "sha256": "3f5232af2ebb12003c833e6bc6643b4858950ebf015435a9b850c6a826fe27d0" + }, + "manifest.json": { + "bytes": 37083, + "sha256": "c23269443cd059895c672330720a10508ab8ecade0d3686ec3d82ff792c9b457" + }, + "evidence.zip": { + "bytes": 25914867, + "sha256": "261660dd8a5e93d917c1d36cad19d1eec0b93e312d1e0dd15f9ea16f5e11c864" + } +} diff --git a/harness/experiments/multiselect-20260923/checks/manifest.json b/harness/experiments/multiselect-20260923/checks/manifest.json new file mode 100644 index 00000000..3ea970aa --- /dev/null +++ b/harness/experiments/multiselect-20260923/checks/manifest.json @@ -0,0 +1,935 @@ +{ + "schema": 1, + "kind": "multiselect-cargo-gates", + "runs": { + "core-1": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-core", + "--test", + "group_movement", + "--test", + "state_diagrams", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 0, + "inputCount": 722, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "core-2": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-core", + "--test", + "group_movement", + "--test", + "state_diagrams", + "--example", + "multiselect_native", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 101, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "core-3": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-core", + "--test", + "group_movement", + "--test", + "state_diagrams", + "--example", + "multiselect_native", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "desktop-1": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-desktop", + "multiselect", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 101, + "inputCount": 727, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "desktop-2": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-desktop", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 101, + "inputCount": 728, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "desktop-3": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-desktop", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "desktop-4": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-desktop", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "desktop-5": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-desktop", + "--", + "--nocapture" + ], + "head": "12a7352ddef5f69a144ead8219a50916233d63a9", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "format-1": { + "command": [ + "cargo", + "fmt", + "--all", + "--", + "--check" + ], + "head": "12a7352ddef5f69a144ead8219a50916233d63a9", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "format-2": { + "command": [ + "cargo", + "fmt", + "--all", + "--", + "--check" + ], + "head": "f6a6010baed9e6164a1fcc7ecd58256a562827d7", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "lint-1": { + "command": [ + "cargo", + "clippy", + "--locked", + "--offline", + "-p", + "oa-core", + "-p", + "oa-desktop", + "--all-targets", + "--", + "-D", + "warnings" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 101, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "lint-2": { + "command": [ + "cargo", + "clippy", + "--locked", + "--offline", + "-p", + "oa-core", + "-p", + "oa-desktop", + "--all-targets", + "--", + "-D", + "warnings" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "lint-3": { + "command": [ + "cargo", + "clippy", + "--locked", + "--offline", + "-p", + "oa-core", + "-p", + "oa-desktop", + "--all-targets", + "--", + "-D", + "warnings" + ], + "head": "12a7352ddef5f69a144ead8219a50916233d63a9", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "native-build-1": { + "command": [ + "cargo", + "build", + "--locked", + "--offline", + "-p", + "oa-core", + "--example", + "multiselect_native", + "--message-format=json" + ], + "head": "12a7352ddef5f69a144ead8219a50916233d63a9", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "native-build-2": { + "command": [ + "cargo", + "build", + "--locked", + "--offline", + "-p", + "oa-core", + "--example", + "multiselect_native", + "--message-format=json" + ], + "head": "f6a6010baed9e6164a1fcc7ecd58256a562827d7", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "oracle-2": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-core", + "--example", + "multiselect_native", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 101, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "oracle-3": { + "command": [ + "cargo", + "test", + "--locked", + "--offline", + "-p", + "oa-core", + "--example", + "multiselect_native", + "--", + "--nocapture" + ], + "head": "b8685416f56e53e30bd11ecf93a54cea10ac8458", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "windows-build-1": { + "command": [ + "cargo", + "build", + "--locked", + "--offline", + "-p", + "oa-desktop", + "--bin", + "arcadia-studio", + "--message-format=json" + ], + "head": "12a7352ddef5f69a144ead8219a50916233d63a9", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "windows-build-2": { + "command": [ + "cargo", + "build", + "--locked", + "--offline", + "-p", + "oa-desktop", + "--bin", + "arcadia-studio", + "--message-format=json" + ], + "head": "f6a6010baed9e6164a1fcc7ecd58256a562827d7", + "exitCode": 0, + "inputCount": 729, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + } + }, + "logicalFiles": { + "cargo/core-1/inputs.json": "capsules/a73d565f90b14c986e0f87a7ac3d368c93e053b7e591cffdfa20d8e81e2eae08.json", + "cargo/core-1/inputs.zip": "capsules/2dcb54b1dd51a05cdd983432b114da8a08dce326c42b6e77a6b8772e75d2282f.zip", + "cargo/core-1/process.json": "cargo/core-1/process.json", + "cargo/core-1/result.json": "cargo/core-1/result.json", + "cargo/core-1/started.json": "cargo/core-1/started.json", + "cargo/core-1/stderr.log": "cargo/core-1/stderr.log", + "cargo/core-1/stdout.log": "cargo/core-1/stdout.log", + "cargo/core-2/inputs.json": "capsules/c5b23cd340869d57bd1bc4400041d499ea06a1a7619a6fd6b51e75278c1755e1.json", + "cargo/core-2/inputs.zip": "capsules/df441a733bf28d1c59eab52063ca4b2645067b490b0a2f0072f98fc9225ea2e8.zip", + "cargo/core-2/process.json": "cargo/core-2/process.json", + "cargo/core-2/result.json": "cargo/core-2/result.json", + "cargo/core-2/started.json": "cargo/core-2/started.json", + "cargo/core-2/stderr.log": "cargo/core-2/stderr.log", + "cargo/core-2/stdout.log": "cargo/core-2/stdout.log", + "cargo/core-3/inputs.json": "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json", + "cargo/core-3/inputs.zip": "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip", + "cargo/core-3/process.json": "cargo/core-3/process.json", + "cargo/core-3/result.json": "cargo/core-3/result.json", + "cargo/core-3/started.json": "cargo/core-3/started.json", + "cargo/core-3/stderr.log": "cargo/core-3/stderr.log", + "cargo/core-3/stdout.log": "cargo/core-3/stdout.log", + "cargo/desktop-1/inputs.json": "capsules/c1083b630ad79ed1fea6933de3221f05b8eb9200694fb0e5dbd637527803579c.json", + "cargo/desktop-1/inputs.zip": "capsules/8d6b08c2b1220cdb48d32cc74004cc9b82e4385f6f50a764f76c4a8630d070bf.zip", + "cargo/desktop-1/process.json": "cargo/desktop-1/process.json", + "cargo/desktop-1/result.json": "cargo/desktop-1/result.json", + "cargo/desktop-1/started.json": "cargo/desktop-1/started.json", + "cargo/desktop-1/stderr.log": "cargo/desktop-1/stderr.log", + "cargo/desktop-1/stdout.log": "cargo/desktop-1/stdout.log", + "cargo/desktop-2/inputs.json": "capsules/5f049dbbee443af86cde70efc74027f290a9bfbd434e5d644aa74ddb4e97566b.json", + "cargo/desktop-2/inputs.zip": "capsules/bf7dc41142e6d087f213b2914e0ffb0e9cd52294f5830301e5e5b78c3470f725.zip", + "cargo/desktop-2/process.json": "cargo/desktop-2/process.json", + "cargo/desktop-2/result.json": "cargo/desktop-2/result.json", + "cargo/desktop-2/started.json": "cargo/desktop-2/started.json", + "cargo/desktop-2/stderr.log": "cargo/desktop-2/stderr.log", + "cargo/desktop-2/stdout.log": "cargo/desktop-2/stdout.log", + "cargo/desktop-3/inputs.json": "capsules/6f8eae7613666bca412e62f67151d6fee9f5bd32fb50363d0ff9b9ac8b190acf.json", + "cargo/desktop-3/inputs.zip": "capsules/cc6233ddec2db0e322659a73a55e1681f5cb3554750ba183a756a29dadf8a8c9.zip", + "cargo/desktop-3/process.json": "cargo/desktop-3/process.json", + "cargo/desktop-3/result.json": "cargo/desktop-3/result.json", + "cargo/desktop-3/started.json": "cargo/desktop-3/started.json", + "cargo/desktop-3/stderr.log": "cargo/desktop-3/stderr.log", + "cargo/desktop-3/stdout.log": "cargo/desktop-3/stdout.log", + "cargo/desktop-4/inputs.json": "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json", + "cargo/desktop-4/inputs.zip": "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip", + "cargo/desktop-4/process.json": "cargo/desktop-4/process.json", + "cargo/desktop-4/result.json": "cargo/desktop-4/result.json", + "cargo/desktop-4/started.json": "cargo/desktop-4/started.json", + "cargo/desktop-4/stderr.log": "cargo/desktop-4/stderr.log", + "cargo/desktop-4/stdout.log": "cargo/desktop-4/stdout.log", + "cargo/desktop-5/inputs.json": "capsules/99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c.json", + "cargo/desktop-5/inputs.zip": "capsules/aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9.zip", + "cargo/desktop-5/process.json": "cargo/desktop-5/process.json", + "cargo/desktop-5/result.json": "cargo/desktop-5/result.json", + "cargo/desktop-5/started.json": "cargo/desktop-5/started.json", + "cargo/desktop-5/stderr.log": "cargo/desktop-5/stderr.log", + "cargo/desktop-5/stdout.log": "cargo/desktop-5/stdout.log", + "cargo/format-1/inputs.json": "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json", + "cargo/format-1/inputs.zip": "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip", + "cargo/format-1/process.json": "cargo/format-1/process.json", + "cargo/format-1/result.json": "cargo/format-1/result.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/format-2/inputs.json": "capsules/99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c.json", + "cargo/format-2/inputs.zip": "capsules/aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9.zip", + "cargo/format-2/process.json": "cargo/format-2/process.json", + "cargo/format-2/result.json": "cargo/format-2/result.json", + "cargo/format-2/started.json": "cargo/format-2/started.json", + "cargo/format-2/stderr.log": "cargo/format-2/stderr.log", + "cargo/format-2/stdout.log": "cargo/format-2/stdout.log", + "cargo/lint-1/inputs.json": "capsules/292f6139450109875549801a1da337dac08a8bca4cfa0124139a5d8d042386b9.json", + "cargo/lint-1/inputs.zip": "capsules/d2ec5025067db2c05736bb1c9c9b6250d9199f1b7190e8c09f5a777424790c10.zip", + "cargo/lint-1/process.json": "cargo/lint-1/process.json", + "cargo/lint-1/result.json": "cargo/lint-1/result.json", + "cargo/lint-1/started.json": "cargo/lint-1/started.json", + "cargo/lint-1/stderr.log": "cargo/lint-1/stderr.log", + "cargo/lint-1/stdout.log": "cargo/lint-1/stdout.log", + "cargo/lint-2/inputs.json": "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json", + "cargo/lint-2/inputs.zip": "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip", + "cargo/lint-2/process.json": "cargo/lint-2/process.json", + "cargo/lint-2/result.json": "cargo/lint-2/result.json", + "cargo/lint-2/started.json": "cargo/lint-2/started.json", + "cargo/lint-2/stderr.log": "cargo/lint-2/stderr.log", + "cargo/lint-2/stdout.log": "cargo/lint-2/stdout.log", + "cargo/lint-3/inputs.json": "capsules/99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c.json", + "cargo/lint-3/inputs.zip": "capsules/aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9.zip", + "cargo/lint-3/process.json": "cargo/lint-3/process.json", + "cargo/lint-3/result.json": "cargo/lint-3/result.json", + "cargo/lint-3/started.json": "cargo/lint-3/started.json", + "cargo/lint-3/stderr.log": "cargo/lint-3/stderr.log", + "cargo/lint-3/stdout.log": "cargo/lint-3/stdout.log", + "cargo/native-build-1/inputs.json": "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json", + "cargo/native-build-1/inputs.zip": "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip", + "cargo/native-build-1/process.json": "cargo/native-build-1/process.json", + "cargo/native-build-1/result.json": "cargo/native-build-1/result.json", + "cargo/native-build-1/started.json": "cargo/native-build-1/started.json", + "cargo/native-build-1/stderr.log": "cargo/native-build-1/stderr.log", + "cargo/native-build-1/stdout.log": "cargo/native-build-1/stdout.log", + "cargo/native-build-2/inputs.json": "capsules/99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c.json", + "cargo/native-build-2/inputs.zip": "capsules/aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9.zip", + "cargo/native-build-2/process.json": "cargo/native-build-2/process.json", + "cargo/native-build-2/result.json": "cargo/native-build-2/result.json", + "cargo/native-build-2/started.json": "cargo/native-build-2/started.json", + "cargo/native-build-2/stderr.log": "cargo/native-build-2/stderr.log", + "cargo/native-build-2/stdout.log": "cargo/native-build-2/stdout.log", + "cargo/oracle-2/inputs.json": "capsules/ae16337dbc617e26b23468adb86d963e3bcf7eddf177e8fb4654c69019904548.json", + "cargo/oracle-2/inputs.zip": "capsules/e99bcaf1ab2becbcad0a005bdd0a4eb14fad2075bd7a38433857df4d3cad5e4d.zip", + "cargo/oracle-2/process.json": "cargo/oracle-2/process.json", + "cargo/oracle-2/result.json": "cargo/oracle-2/result.json", + "cargo/oracle-2/started.json": "cargo/oracle-2/started.json", + "cargo/oracle-2/stderr.log": "cargo/oracle-2/stderr.log", + "cargo/oracle-2/stdout.log": "cargo/oracle-2/stdout.log", + "cargo/oracle-3/inputs.json": "capsules/292f6139450109875549801a1da337dac08a8bca4cfa0124139a5d8d042386b9.json", + "cargo/oracle-3/inputs.zip": "capsules/d2ec5025067db2c05736bb1c9c9b6250d9199f1b7190e8c09f5a777424790c10.zip", + "cargo/oracle-3/process.json": "cargo/oracle-3/process.json", + "cargo/oracle-3/result.json": "cargo/oracle-3/result.json", + "cargo/oracle-3/started.json": "cargo/oracle-3/started.json", + "cargo/oracle-3/stderr.log": "cargo/oracle-3/stderr.log", + "cargo/oracle-3/stdout.log": "cargo/oracle-3/stdout.log", + "cargo/windows-build-1/inputs.json": "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json", + "cargo/windows-build-1/inputs.zip": "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip", + "cargo/windows-build-1/process.json": "cargo/windows-build-1/process.json", + "cargo/windows-build-1/result.json": "cargo/windows-build-1/result.json", + "cargo/windows-build-1/started.json": "cargo/windows-build-1/started.json", + "cargo/windows-build-1/stderr.log": "cargo/windows-build-1/stderr.log", + "cargo/windows-build-1/stdout.log": "cargo/windows-build-1/stdout.log", + "cargo/windows-build-2/inputs.json": "capsules/99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c.json", + "cargo/windows-build-2/inputs.zip": "capsules/aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9.zip", + "cargo/windows-build-2/process.json": "cargo/windows-build-2/process.json", + "cargo/windows-build-2/result.json": "cargo/windows-build-2/result.json", + "cargo/windows-build-2/started.json": "cargo/windows-build-2/started.json", + "cargo/windows-build-2/stderr.log": "cargo/windows-build-2/stderr.log", + "cargo/windows-build-2/stdout.log": "cargo/windows-build-2/stdout.log", + "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "members": { + "capsules/292f6139450109875549801a1da337dac08a8bca4cfa0124139a5d8d042386b9.json": { + "bytes": 121985, + "sha256": "292f6139450109875549801a1da337dac08a8bca4cfa0124139a5d8d042386b9" + }, + "capsules/2dcb54b1dd51a05cdd983432b114da8a08dce326c42b6e77a6b8772e75d2282f.zip": { + "bytes": 2885634, + "sha256": "2dcb54b1dd51a05cdd983432b114da8a08dce326c42b6e77a6b8772e75d2282f" + }, + "capsules/55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb.json": { + "bytes": 121985, + "sha256": "55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb" + }, + "capsules/5f049dbbee443af86cde70efc74027f290a9bfbd434e5d644aa74ddb4e97566b.json": { + "bytes": 121820, + "sha256": "5f049dbbee443af86cde70efc74027f290a9bfbd434e5d644aa74ddb4e97566b" + }, + "capsules/6f8eae7613666bca412e62f67151d6fee9f5bd32fb50363d0ff9b9ac8b190acf.json": { + "bytes": 121984, + "sha256": "6f8eae7613666bca412e62f67151d6fee9f5bd32fb50363d0ff9b9ac8b190acf" + }, + "capsules/8d6b08c2b1220cdb48d32cc74004cc9b82e4385f6f50a764f76c4a8630d070bf.zip": { + "bytes": 2903240, + "sha256": "8d6b08c2b1220cdb48d32cc74004cc9b82e4385f6f50a764f76c4a8630d070bf" + }, + "capsules/99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c.json": { + "bytes": 121985, + "sha256": "99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c" + }, + "capsules/a73d565f90b14c986e0f87a7ac3d368c93e053b7e591cffdfa20d8e81e2eae08.json": { + "bytes": 120844, + "sha256": "a73d565f90b14c986e0f87a7ac3d368c93e053b7e591cffdfa20d8e81e2eae08" + }, + "capsules/aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9.zip": { + "bytes": 2918930, + "sha256": "aa8bb3d7fbd1f73c36913235b0b1d3aa79f6ac418b6c07ade94a7dee4a617bd9" + }, + "capsules/ae16337dbc617e26b23468adb86d963e3bcf7eddf177e8fb4654c69019904548.json": { + "bytes": 121984, + "sha256": "ae16337dbc617e26b23468adb86d963e3bcf7eddf177e8fb4654c69019904548" + }, + "capsules/bf7dc41142e6d087f213b2914e0ffb0e9cd52294f5830301e5e5b78c3470f725.zip": { + "bytes": 2909281, + "sha256": "bf7dc41142e6d087f213b2914e0ffb0e9cd52294f5830301e5e5b78c3470f725" + }, + "capsules/c1083b630ad79ed1fea6933de3221f05b8eb9200694fb0e5dbd637527803579c.json": { + "bytes": 121652, + "sha256": "c1083b630ad79ed1fea6933de3221f05b8eb9200694fb0e5dbd637527803579c" + }, + "capsules/c5b23cd340869d57bd1bc4400041d499ea06a1a7619a6fd6b51e75278c1755e1.json": { + "bytes": 121984, + "sha256": "c5b23cd340869d57bd1bc4400041d499ea06a1a7619a6fd6b51e75278c1755e1" + }, + "capsules/cc6233ddec2db0e322659a73a55e1681f5cb3554750ba183a756a29dadf8a8c9.zip": { + "bytes": 2917494, + "sha256": "cc6233ddec2db0e322659a73a55e1681f5cb3554750ba183a756a29dadf8a8c9" + }, + "capsules/d2ec5025067db2c05736bb1c9c9b6250d9199f1b7190e8c09f5a777424790c10.zip": { + "bytes": 2918296, + "sha256": "d2ec5025067db2c05736bb1c9c9b6250d9199f1b7190e8c09f5a777424790c10" + }, + "capsules/df441a733bf28d1c59eab52063ca4b2645067b490b0a2f0072f98fc9225ea2e8.zip": { + "bytes": 2917518, + "sha256": "df441a733bf28d1c59eab52063ca4b2645067b490b0a2f0072f98fc9225ea2e8" + }, + "capsules/e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533.zip": { + "bytes": 2918309, + "sha256": "e1c7f3abe7b32616d1081b4a2bb8af2d6d4b071bc2a7cbdd61e96a92ffdc0533" + }, + "capsules/e99bcaf1ab2becbcad0a005bdd0a4eb14fad2075bd7a38433857df4d3cad5e4d.zip": { + "bytes": 2917719, + "sha256": "e99bcaf1ab2becbcad0a005bdd0a4eb14fad2075bd7a38433857df4d3cad5e4d" + }, + "cargo/core-1/process.json": { + "bytes": 210, + "sha256": "92e5a88b5b86400c13bad4d0a9d490466bb2ef0b6077d14930ac5084187b5506" + }, + "cargo/core-1/result.json": { + "bytes": 1821, + "sha256": "c90e020e6d912516fbc5ef5b4bf6a5df5fc668e5f5e19a9108c29bf3723cea9c" + }, + "cargo/core-1/started.json": { + "bytes": 1428, + "sha256": "230ea369285569a0fa179fe420b0b2133e7fcb2349601a53d69881e9a66bd39b" + }, + "cargo/core-1/stderr.log": { + "bytes": 434, + "sha256": "84a1b0b7352dcde2fe51293feabbddf8e94206d9278e4a38e6d354af8a1bcd48" + }, + "cargo/core-1/stdout.log": { + "bytes": 1671, + "sha256": "eb04561c11e394cc368ea1c5e38aa4b1dc8b5c3c44acb168469dd941f80f14e8" + }, + "cargo/core-2/process.json": { + "bytes": 245, + "sha256": "020d6cb0ce6e4b9122ac2dcbd86c9c1719479f29d3690247182ade858caba12d" + }, + "cargo/core-2/result.json": { + "bytes": 1870, + "sha256": "c97cbcd85b60a3476135bf9026d41868ea091061c12ce4bbc18a40c6023c5b3e" + }, + "cargo/core-2/started.json": { + "bytes": 1473, + "sha256": "f367b289c9d378975514fa450e4fa25049f4461156417ba5bd6e79836faf401e" + }, + "cargo/core-2/stderr.log": { + "bytes": 5287, + "sha256": "4ea6f38eef014ab8176c3ab1ed9d3be3addb4463a8f073425f1b5493b33a7c86" + }, + "cargo/core-2/stdout.log": { + "bytes": 4138, + "sha256": "a2dfdb0d7c6c7ba7e9ef86063a815f43111188a44ae4b0e20717cc3e518b7c3d" + }, + "cargo/core-3/process.json": { + "bytes": 245, + "sha256": "b26f22b0aed07e55906b024d1fd94198da1ab1d56363fc5ad66868940ae1c9a4" + }, + "cargo/core-3/result.json": { + "bytes": 1867, + "sha256": "6d04a2818750491cb73ebb1b257d214316c048e1ac6165a119a27534d636406c" + }, + "cargo/core-3/started.json": { + "bytes": 1473, + "sha256": "49e9d637eb063277925c577266a63e5fd06723ca24726d609dc0e458596e8316" + }, + "cargo/core-3/stderr.log": { + "bytes": 539, + "sha256": "f05f7ab2f0244968100eb0e0a82808af8e42dceb5b5de70d034b7ae56bad9489" + }, + "cargo/core-3/stdout.log": { + "bytes": 4797, + "sha256": "d0f2180aaecc3b253b473200b77348a19a21594e193ce45c21f040b7242be253" + }, + "cargo/desktop-1/process.json": { + "bytes": 172, + "sha256": "d9ba2b6a0968b702af0aa857e7bb8667680eeddc32a0403781cd3d5e2467c067" + }, + "cargo/desktop-1/result.json": { + "bytes": 1769, + "sha256": "2384c076e4bbc0e15951671e3a830725213c31cfcded0a27e1bb72802c576dbc" + }, + "cargo/desktop-1/started.json": { + "bytes": 1375, + "sha256": "6b1ba478ebe5e14d5ee76199a56874a6a10156c2dd7e9d3830fd5ba5311094d6" + }, + "cargo/desktop-1/stderr.log": { + "bytes": 4604, + "sha256": "6a47286c4daaf9f4ac8ac8cca3e98931f7812a44b192128aa67880c1ac930b87" + }, + "cargo/desktop-1/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/desktop-2/process.json": { + "bytes": 157, + "sha256": "cc037bab1ec44bd2e4095e165597f5e1d077bae5fce7a96c6a5fd814470d4244" + }, + "cargo/desktop-2/result.json": { + "bytes": 1753, + "sha256": "9c037405572273086737e22b60796eff0745f4746ee3f25529f928da53c98fbc" + }, + "cargo/desktop-2/started.json": { + "bytes": 1355, + "sha256": "18f71e571722bdb3696195623b3af3a2b9a45d38f98307a68049aacff3fbad9c" + }, + "cargo/desktop-2/stderr.log": { + "bytes": 1085, + "sha256": "316e0179675fb071d7d9773d7073d771438ecd70e66c99559f4239e0638a3238" + }, + "cargo/desktop-2/stdout.log": { + "bytes": 145051, + "sha256": "7c907317d106ff63347456b5ad2015fa8a9974ffbb7003d0a123afc99879ce42" + }, + "cargo/desktop-3/process.json": { + "bytes": 157, + "sha256": "368d265e1ee5cceeb5995172c662dc3bc551591b9600a781c63ed64579760c24" + }, + "cargo/desktop-3/result.json": { + "bytes": 1751, + "sha256": "32076fb49a6ddfcef63e77ca9f1edec34700ca80d36f32890581d56035f80054" + }, + "cargo/desktop-3/started.json": { + "bytes": 1355, + "sha256": "da1ccd8352380b357d5954a70d86f2c51c8a2da8d1c4b553ca7b281d3000eab9" + }, + "cargo/desktop-3/stderr.log": { + "bytes": 626, + "sha256": "5333e25e78f515449877867d0a2bf714b7cb3302c9497d4f4343a1e05e0f8b03" + }, + "cargo/desktop-3/stdout.log": { + "bytes": 145562, + "sha256": "dc7e40a7d2ab1e78c74f0ee0344c4b2e5b1402bc6367e93a40780774000e4f81" + }, + "cargo/desktop-4/process.json": { + "bytes": 157, + "sha256": "0823d0fbda95fe5efb08a7c11c98a7ac2409b5b3d170cf2e41b702895211abf4" + }, + "cargo/desktop-4/result.json": { + "bytes": 1750, + "sha256": "d88b177e9d181f7c9c3685100c8676c0c8954cd0b071344ff5b4bb7a8e498865" + }, + "cargo/desktop-4/started.json": { + "bytes": 1355, + "sha256": "0f9927d5c0b09c7fcc8922c68a2c88a4d840e0420a7be90193f566b02312a3d3" + }, + "cargo/desktop-4/stderr.log": { + "bytes": 628, + "sha256": "992db238282b7acf1bbfeee7f2c459f9afe4bce814761d70df789e4f6239afcf" + }, + "cargo/desktop-4/stdout.log": { + "bytes": 145802, + "sha256": "70c38e8c98cac4f220999481908beb549e5377f6f48c31931f9412ed25a67a41" + }, + "cargo/desktop-5/process.json": { + "bytes": 157, + "sha256": "83bf9efc58fa5692f3ed038c107c081e5483d91117c73eddba62d3eb45cc7267" + }, + "cargo/desktop-5/result.json": { + "bytes": 1751, + "sha256": "7cbbae8bbceec07cebb4696b999701cd29d46bfd047351aa3e20ea49d7c67ae6" + }, + "cargo/desktop-5/started.json": { + "bytes": 1355, + "sha256": "0bfd2a6c185afb1b301e3b6b5f746d0e37251f147590aa2e9f25e370e99f53e8" + }, + "cargo/desktop-5/stderr.log": { + "bytes": 631, + "sha256": "466f26c41956ed05629b633827d7ffca497c57c87ff67acff6351043c7bdefd4" + }, + "cargo/desktop-5/stdout.log": { + "bytes": 146577, + "sha256": "e89cf5f2375fd40ae70261471d797cbaca5127cb27730e6094a616afa12514ae" + }, + "cargo/format-1/process.json": { + "bytes": 116, + "sha256": "bb8e0a7620b40bb5f0932643848a57b0d207dde39006a9a20141c00f13d94284" + }, + "cargo/format-1/result.json": { + "bytes": 1687, + "sha256": "967eeddb3fc6a0dc0fdf9a75f575d75fa8f8b0b5ea9c8a422590bcbece6de77b" + }, + "cargo/format-1/started.json": { + "bytes": 1299, + "sha256": "ceb354dfbc4a41d72e496342e28475ddcce5a3c17a4e1553f0d699404c374657" + }, + "cargo/format-1/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/format-1/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/format-2/process.json": { + "bytes": 116, + "sha256": "aa31b91d224845b7dd1df777e865c88c5b8f55b66f173053be318132d1bc3870" + }, + "cargo/format-2/result.json": { + "bytes": 1687, + "sha256": "c4d5529c8d2feae1b805bd8d404a3817053a5a0cd4ec341aa8c1e0a5c05eed5b" + }, + "cargo/format-2/started.json": { + "bytes": 1299, + "sha256": "2ea70bb29d7fef73a194acd7aa36836fd4a54ae8a4be173f919cd2fc32e97919" + }, + "cargo/format-2/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/format-2/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/lint-1/process.json": { + "bytes": 196, + "sha256": "d30e400277dc0bb4a1618689b16ab86be7838cc1b42688185d655df235080286" + }, + "cargo/lint-1/result.json": { + "bytes": 1808, + "sha256": "7ffb361d398def4520afc48683a69effbb8b066efd9442ba3b356c8124aae6d9" + }, + "cargo/lint-1/started.json": { + "bytes": 1414, + "sha256": "771b5bdc31662fb3b4f76735e92b0e89d31a02188fbce35c5cfb0aea4c37c0ca" + }, + "cargo/lint-1/stderr.log": { + "bytes": 1023, + "sha256": "9cbab357d810ce8164a0e614491f53529e61c1cad79793ca429e13897a8dc5ed" + }, + "cargo/lint-1/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/lint-2/process.json": { + "bytes": 196, + "sha256": "f4aed65014ed120dc72bd08160559ae0285bc5c4968305a5fb86cf29e8c0d347" + }, + "cargo/lint-2/result.json": { + "bytes": 1805, + "sha256": "b873add4777fd8f7516513e2423f9d3eb4a40505a3b1fa7edfbee89fb5c7e0d3" + }, + "cargo/lint-2/started.json": { + "bytes": 1414, + "sha256": "0d01a20ec0f7cd796c066c95936c74df0abf8947fca655d900a5bdd4f8dbd610" + }, + "cargo/lint-2/stderr.log": { + "bytes": 207, + "sha256": "e6f76746d5864a4de1e9d238c74799e2080e2c6214f51b68a4692d5e15f5b107" + }, + "cargo/lint-2/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/lint-3/process.json": { + "bytes": 196, + "sha256": "620094b52dab8f23f389687cf72b8b3aa4effa55697842206a618293537eb869" + }, + "cargo/lint-3/result.json": { + "bytes": 1805, + "sha256": "1e7dc472c03d653094850ed653599cee41e50d1639389bbc3fee45e40a754b5c" + }, + "cargo/lint-3/started.json": { + "bytes": 1414, + "sha256": "b921ee7102386187c288c1b25aea96a3019ff00a4a508065aefbc4499b7c7818" + }, + "cargo/lint-3/stderr.log": { + "bytes": 137, + "sha256": "d19e47547a69ce7956c74f2a65cd1064571985d073bcf497a11a0b0eebcfbd04" + }, + "cargo/lint-3/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/native-build-1/process.json": { + "bytes": 194, + "sha256": "390cb979d680b9fdb7e0afa05d6b9d10a25dc27abaff16596d113bb0b2142b43" + }, + "cargo/native-build-1/result.json": { + "bytes": 3132, + "sha256": "35f76499ac2a49446b896932c963fd8734a5755e00e3511cedf1e584e7b17704" + }, + "cargo/native-build-1/started.json": { + "bytes": 1397, + "sha256": "5f37fc095aa00eae8fae043651a29f387f484bf4a0cc454e3919ea4f49567126" + }, + "cargo/native-build-1/stderr.log": { + "bytes": 130, + "sha256": "731d61ee089d75c107edcd02d2cdedcddaad57ec06607ccd0822d9dc71a72720" + }, + "cargo/native-build-1/stdout.log": { + "bytes": 41700, + "sha256": "832dc599cad2c9ba28ff9153537fb428390d8f25959f5b4a080cd432f6bea0e8" + }, + "cargo/native-build-2/process.json": { + "bytes": 194, + "sha256": "46ff130840d8b12272be43f479adeb0b2ace94ee1d5389c5638a61b22d732496" + }, + "cargo/native-build-2/result.json": { + "bytes": 3131, + "sha256": "fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b" + }, + "cargo/native-build-2/started.json": { + "bytes": 1397, + "sha256": "cbd2c7ad98baddc3719a5c03bef103747d4cb5c560d631376149f116ede49584" + }, + "cargo/native-build-2/stderr.log": { + "bytes": 60, + "sha256": "b2558545bbfa50a0f0c26623c0982eb5f96d71ea228d7c56180edbac0d0b295d" + }, + "cargo/native-build-2/stdout.log": { + "bytes": 41699, + "sha256": "7884b8ea1a92d58c7174657560ec38ff9a073e52f93cc19a78290840f2ab5486" + }, + "cargo/oracle-2/process.json": { + "bytes": 189, + "sha256": "1e8ffd7a03b22bf2ec43b0d66ae3cc98016a431c4a07499faf3ca0c635b92337" + }, + "cargo/oracle-2/result.json": { + "bytes": 1789, + "sha256": "e0b7331751bae8564ee5c28d05029071f1c8460195e6f9e2f0d00a5ae4e05a16" + }, + "cargo/oracle-2/started.json": { + "bytes": 1397, + "sha256": "b7a8278dfa2324f0c3ff6e9616a65ef7a51c99699ad700adfe90e2accc435765" + }, + "cargo/oracle-2/stderr.log": { + "bytes": 520, + "sha256": "9dd8b849afdbbb2207a9ad82f1b6c6f07fd9cd0846fe4591f33c40fdadcbb3ac" + }, + "cargo/oracle-2/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/oracle-3/process.json": { + "bytes": 189, + "sha256": "a2cb30f512ee390bb03c9fcf5ea8069ca3a05da91f798985efc3f9f54670ace4" + }, + "cargo/oracle-3/result.json": { + "bytes": 1791, + "sha256": "f0768fecc9f6711dd3cfc627c60aaf6118216bce47e76faa4cbdda571ac675e4" + }, + "cargo/oracle-3/started.json": { + "bytes": 1397, + "sha256": "30cf614b6dede363ae4bdeca56fc4a1ddb7ee19a7e592b1b1616d4a98a53b535" + }, + "cargo/oracle-3/stderr.log": { + "bytes": 307, + "sha256": "b4ccc583c5001d81b7208fe6f2e52788d0d40b63c7da034ca1fb90ec2adb18a4" + }, + "cargo/oracle-3/stdout.log": { + "bytes": 2968, + "sha256": "e0a62ca8b008370c5a41378eb4c9e2a229a2fcde7bdba55426313c7d23da8aca" + }, + "cargo/windows-build-1/process.json": { + "bytes": 189, + "sha256": "4e6b57dc0d5f10ac1b4af991f8b0f7558f4fc5b626e00f1a49c5782ea13abe18" + }, + "cargo/windows-build-1/result.json": { + "bytes": 3082, + "sha256": "3cdd09a859288afe71dde6fb51bcc4bca84e47ceb9cb6389d57f6722d9e23fdf" + }, + "cargo/windows-build-1/started.json": { + "bytes": 1392, + "sha256": "a3ebcdffc01e93fd5609005a0539eb11bf473b45bc28eee71dd175b943b387eb" + }, + "cargo/windows-build-1/stderr.log": { + "bytes": 319, + "sha256": "7201ff3f71fe793907482992a00682023fc6ee1ac0a016f53a048b299ec2fffd" + }, + "cargo/windows-build-1/stdout.log": { + "bytes": 284182, + "sha256": "3fc90b8c923415496c7990d33a5fe7211423b83df833a80111079fad71f04a2d" + }, + "cargo/windows-build-2/process.json": { + "bytes": 189, + "sha256": "f18acb4c9feede438e1aa67bb86ce2d74f5b3aa0142499ae99b56b23daaf834b" + }, + "cargo/windows-build-2/result.json": { + "bytes": 3081, + "sha256": "d7b191ee07c302ffed1017374b0c5d8b86245c0f3348fbac4778ae573397b9de" + }, + "cargo/windows-build-2/started.json": { + "bytes": 1392, + "sha256": "b6a6656d966559060f01e87ccbd75837b0cf67e70c77246b20f587167bdbf651" + }, + "cargo/windows-build-2/stderr.log": { + "bytes": 137, + "sha256": "aafe81cdd7c1e5558e957d4296d46e3238c72af79ba79d6147c5cc16f938d6ef" + }, + "cargo/windows-build-2/stdout.log": { + "bytes": 284179, + "sha256": "cfba733cb79a9496004109bb0bc8b8996df5ec1645691fd79b952063faa519c5" + }, + "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + } + } +} diff --git a/harness/experiments/multiselect-20260923/checks/prepare.py b/harness/experiments/multiselect-20260923/checks/prepare.py new file mode 100644 index 00000000..ecaa6075 --- /dev/null +++ b/harness/experiments/multiselect-20260923/checks/prepare.py @@ -0,0 +1,90 @@ +"""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): + gates = root / ".local/group-movement-gates" + candidates = sorted(gates.glob("*.py")) + helpers + helper_bytes = [path.read_bytes() for path in candidates] + 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 + for run in runs: + folder = gates / run + need(folder.is_dir(), "run directory missing: " + run) + paths = list(folder.iterdir()) + need(all(path.is_file() and not path.is_symlink() for path in paths) + and {path.name for path in paths} == RAW_FILES, "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 + manifest = {"schema": 1, "kind": "multiselect-cargo-gates", "runs": specs, + "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, default=HERE.parents[3], help="checkout holding .local/group-movement-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("--output", type=Path, default=HERE, help="package directory; existing evidence is never overwritten") + 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") + payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper) + if args.check_only: + print("PASS raw consistency; no package written:", summary) + return + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + 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() diff --git a/harness/experiments/multiselect-20260923/checks/verify.py b/harness/experiments/multiselect-20260923/checks/verify.py new file mode 100644 index 00000000..2fdb54f5 --- /dev/null +++ b/harness/experiments/multiselect-20260923/checks/verify.py @@ -0,0 +1,164 @@ +"""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"] == "multiselect-cargo-gates", "manifest schema") + runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"] + 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()) + 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]) + 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") + 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), "logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules)} + + +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() diff --git a/harness/experiments/multiselect-20260923/incidents/README.md b/harness/experiments/multiselect-20260923/incidents/README.md new file mode 100644 index 00000000..4fa541b7 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/README.md @@ -0,0 +1,35 @@ +# Run-2 baseline checker environment incident + +The first baseline checker invocation exited `3221225781` (`0xC0000135`) at +2026-09-23 09:07:10 UTC with empty stdout/stderr. The parent reported that this +shell omitted dot-sourcing the toolchain `env.ps1`. After that environment setup, +the same checker command succeeded at 09:08:08 UTC with a complete-Project PASS. +This records a checker-process launch incident, not a failure of the running GUI. +No particular missing DLL was measured or identified. + +`run-2-baseline-runtime-environment/` is an exact copy of the failed snapshot: +started/result receipts, empty streams and all nine source files. Its unchanged +command text still names the original `run-2/snapshots/baseline` location; the +parent moved the failed directory aside before retrying. Paths in these records +are historical labels and have not been rewritten. + +`retry-baseline/` preserves the successful retry's original started/result +receipts and streams. Direct comparison confirmed identical command, build +receipt, binary, helper, baseline and source pins between attempts. All three +unchanged flags are true in both results. The nine retry source files were +also compared byte for byte with the failed snapshot and are identical; they +are not duplicated here. The successful run's complete snapshots belong in the +separate native evidence package. + +`helpers/check-snapshot-2.py` matches the helper pin in both receipts. +`helpers/env.ps1` preserves the environment setup script available when this +incident was packaged. It sets toolchain paths and runtime search paths and +contains no credentials. The checker receipts do not record the environment or +pin `env.ps1`, so the omitted/setup distinction is the parent's invocation +observation, not an independently measured environment comparison. Both scripts +are historical data; this package does not execute them. + +The checker binary pin in both attempts is +`4de47a2b62192be6a1394e654f368be19e36fb966d60d6c833a5d51d18247bb1`. +`files.json` pins every included file except itself. Raw bytes are preserved. +No frozen checks, source-identity or native packaging tools were changed. diff --git a/harness/experiments/multiselect-20260923/incidents/files.json b/harness/experiments/multiselect-20260923/incidents/files.json new file mode 100644 index 00000000..1ae84cbe --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/files.json @@ -0,0 +1,82 @@ +{ + "helpers/check-snapshot-2.py": { + "bytes": 2494, + "sha256": "1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8" + }, + "helpers/env.ps1": { + "bytes": 621, + "sha256": "d7eb6249bef73adacdb05cebefe7ca478758c3c38de17a1731bf01f8b25dc379" + }, + "README.md": { + "bytes": 2146, + "sha256": "d5a853c62713a6c87d28acfbe28c03bd95459275d0a2eb752d978e5a9b427bc8" + }, + "retry-baseline/result.json": { + "bytes": 2758, + "sha256": "ae16cbc54bf5b6054750ef082f47bf0a26ee2dfb7bce46f19c8fd2c1ac622d7f" + }, + "retry-baseline/started.json": { + "bytes": 2356, + "sha256": "e172a36f0872d49a8fe0f7200fb56d597b71fd1849dc9902d53ac1813a3c7d4a" + }, + "retry-baseline/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "retry-baseline/stdout.log": { + "bytes": 441, + "sha256": "12842cc4e483e2546f5c8615b17fe72c5fe07f3fb4440469071433c82d9a9b30" + }, + "run-2-baseline-runtime-environment/result.json": { + "bytes": 2765, + "sha256": "539b950952461cc300876dbc45fa9ee02a9ecefb6d712fce6b74bad3fa701b30" + }, + "run-2-baseline-runtime-environment/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "run-2-baseline-runtime-environment/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "run-2-baseline-runtime-environment/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "run-2-baseline-runtime-environment/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "run-2-baseline-runtime-environment/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "run-2-baseline-runtime-environment/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "run-2-baseline-runtime-environment/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "run-2-baseline-runtime-environment/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "run-2-baseline-runtime-environment/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "run-2-baseline-runtime-environment/started.json": { + "bytes": 2356, + "sha256": "338c14c7dc827bd4ab09009bb974ad3c481b07cdbad2437f5291b1ff67b5ba42" + }, + "run-2-baseline-runtime-environment/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "run-2-baseline-runtime-environment/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } +} diff --git a/harness/experiments/multiselect-20260923/incidents/helpers/check-snapshot-2.py b/harness/experiments/multiselect-20260923/incidents/helpers/check-snapshot-2.py new file mode 100644 index 00000000..440cf1aa --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/helpers/check-snapshot-2.py @@ -0,0 +1,41 @@ +"""Copy stable native-saved source and check its complete Project independently.""" +from pathlib import Path +import datetime, hashlib, json, re, shutil, subprocess, sys +root = Path('C:/ar-multiselect-9e8r6t') +native = root / '.local/multiselect-native' +label, mode, *rest = sys.argv[1:] +assert not rest and re.fullmatch('[a-z0-9-]+', label) +working = native / 'working-2' +out = native / 'run-2/snapshots' / label +out.mkdir(parents=True, exist_ok=False) +gate_path = root / '.local/group-movement-gates/native-build-2/result.json' +gate = json.loads(gate_path.read_bytes()) +assert gate['exitCode'] == 0 and gate['inputsUnchanged'] +artifact, = [a for a in gate['artifacts'] if a['cargo']['target']['name'] == 'multiselect_native'] +binary = Path(artifact['cargo']['executable']) +baseline = native / 'fixture-2/baseline.json' +def pin(path): + b = path.read_bytes() + return {'bytes': len(b), 'sha256': hashlib.sha256(b).hexdigest()} +def inventory(): + return {p.relative_to(working).as_posix():pin(p) for p in sorted(working.rglob('*.arcadia')) if p.is_file()} +def now(): return datetime.datetime.now(datetime.timezone.utc).isoformat() +before = inventory() +assert 'project.arcadia' in before and pin(binary) == artifact['readback'] +for name in before: + target = out/'source'/name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(working/name, target) +assert before == inventory() == {name:pin(out/'source'/name) for name in before} +command = [str(binary), 'check', mode, str(baseline), str(out/'source/project.arcadia')] +start = {'start':now(), 'command':command, 'buildReceipt':pin(gate_path), 'binary':pin(binary), 'helper':pin(Path(__file__)), 'source':before, 'baseline':pin(baseline)} +(out/'started.json').write_text(json.dumps(start,indent=2),encoding='utf-8') +r = subprocess.run(command,cwd=root,capture_output=True) +(out/'stdout.log').write_bytes(r.stdout) +(out/'stderr.log').write_bytes(r.stderr) +result = {**start, 'finish':now(), 'exitCode':r.returncode, 'binaryUnchanged':pin(binary)==artifact['readback'], 'workingSourceUnchanged':before==inventory(), 'baselineUnchanged':pin(baseline)==start['baseline'], 'stdout':pin(out/'stdout.log'), 'stderr':pin(out/'stderr.log')} +(out/'result.json').write_text(json.dumps(result,indent=2),encoding='utf-8') +print(r.stdout.decode(errors='replace')) +print(r.stderr.decode(errors='replace')) +assert result['binaryUnchanged'] and result['workingSourceUnchanged'] and result['baselineUnchanged'] +sys.exit(r.returncode) diff --git a/harness/experiments/multiselect-20260923/incidents/helpers/env.ps1 b/harness/experiments/multiselect-20260923/incidents/helpers/env.ps1 new file mode 100644 index 00000000..18dfd261 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/helpers/env.ps1 @@ -0,0 +1,9 @@ +$taskRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +$env:RUSTUP_HOME = Join-Path $taskRoot '.local\toolchain\rustup' +$env:CARGO_HOME = Join-Path $taskRoot '.local\toolchain\cargo' +$env:RUSTUP_TOOLCHAIN = 'stable-x86_64-pc-windows-gnullvm' +$env:PATH = "$env:CARGO_HOME\bin;$(Join-Path $taskRoot '.local\toolchain\llvm-mingw-20260826-ucrt-x86_64\bin');$env:PATH" +$env:CARGO_TARGET_DIR = Join-Path $taskRoot '.local\target-gnullvm' +# Keep this small development disk from accumulating multi-GiB incremental caches. +$env:CARGO_INCREMENTAL = '0' +$env:CARGO_TARGET_X86_64_PC_WINDOWS_GNULLVM_LINKER = 'clang.exe' diff --git a/harness/experiments/multiselect-20260923/incidents/retry-baseline/result.json b/harness/experiments/multiselect-20260923/incidents/retry-baseline/result.json new file mode 100644 index 00000000..064cc67f --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/retry-baseline/result.json @@ -0,0 +1,77 @@ +{ + "start": "2026-09-23T09:08:07.906733+00:00", + "command": [ + "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\multiselect_native.exe", + "check", + "unchanged", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\fixture-2\\baseline.json", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\run-2\\snapshots\\baseline\\source\\project.arcadia" + ], + "buildReceipt": { + "bytes": 3131, + "sha256": "fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b" + }, + "binary": { + "bytes": 22162432, + "sha256": "4de47a2b62192be6a1394e654f368be19e36fb966d60d6c833a5d51d18247bb1" + }, + "helper": { + "bytes": 2494, + "sha256": "1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8" + }, + "source": { + "oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + } + }, + "baseline": { + "bytes": 8944, + "sha256": "d1278e58cd31e1d61093292eddc1d7b2cc9c8cdc7fe5dd3801de8411af8db89d" + }, + "finish": "2026-09-23T09:08:08.158861+00:00", + "exitCode": 0, + "binaryUnchanged": true, + "workingSourceUnchanged": true, + "baselineUnchanged": true, + "stdout": { + "bytes": 441, + "sha256": "12842cc4e483e2546f5c8615b17fe72c5fe07f3fb4440469071433c82d9a9b30" + }, + "stderr": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } +} \ No newline at end of file diff --git a/harness/experiments/multiselect-20260923/incidents/retry-baseline/started.json b/harness/experiments/multiselect-20260923/incidents/retry-baseline/started.json new file mode 100644 index 00000000..1d0fd9bc --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/retry-baseline/started.json @@ -0,0 +1,64 @@ +{ + "start": "2026-09-23T09:08:07.906733+00:00", + "command": [ + "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\multiselect_native.exe", + "check", + "unchanged", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\fixture-2\\baseline.json", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\run-2\\snapshots\\baseline\\source\\project.arcadia" + ], + "buildReceipt": { + "bytes": 3131, + "sha256": "fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b" + }, + "binary": { + "bytes": 22162432, + "sha256": "4de47a2b62192be6a1394e654f368be19e36fb966d60d6c833a5d51d18247bb1" + }, + "helper": { + "bytes": 2494, + "sha256": "1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8" + }, + "source": { + "oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + } + }, + "baseline": { + "bytes": 8944, + "sha256": "d1278e58cd31e1d61093292eddc1d7b2cc9c8cdc7fe5dd3801de8411af8db89d" + } +} \ No newline at end of file diff --git a/harness/experiments/multiselect-20260923/incidents/retry-baseline/stderr.log b/harness/experiments/multiselect-20260923/incidents/retry-baseline/stderr.log new file mode 100644 index 00000000..e69de29b diff --git a/harness/experiments/multiselect-20260923/incidents/retry-baseline/stdout.log b/harness/experiments/multiselect-20260923/incidents/retry-baseline/stdout.log new file mode 100644 index 00000000..881d37c9 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/retry-baseline/stdout.log @@ -0,0 +1,9 @@ +{ + "baseline": "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\fixture-2\\baseline.json", + "comparison": "complete Project equality", + "inferred_common_delta": null, + "limits": "Invariant evaluator only; no pointer/action/focus/Undo/source-byte acceptance claim", + "mode": "unchanged", + "result": "PASS", + "saved": "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\run-2\\snapshots\\baseline\\source\\project.arcadia" +} diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/result.json b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/result.json new file mode 100644 index 00000000..6d8a206e --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/result.json @@ -0,0 +1,77 @@ +{ + "start": "2026-09-23T09:07:10.546708+00:00", + "command": [ + "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\multiselect_native.exe", + "check", + "unchanged", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\fixture-2\\baseline.json", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\run-2\\snapshots\\baseline\\source\\project.arcadia" + ], + "buildReceipt": { + "bytes": 3131, + "sha256": "fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b" + }, + "binary": { + "bytes": 22162432, + "sha256": "4de47a2b62192be6a1394e654f368be19e36fb966d60d6c833a5d51d18247bb1" + }, + "helper": { + "bytes": 2494, + "sha256": "1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8" + }, + "source": { + "oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + } + }, + "baseline": { + "bytes": 8944, + "sha256": "d1278e58cd31e1d61093292eddc1d7b2cc9c8cdc7fe5dd3801de8411af8db89d" + }, + "finish": "2026-09-23T09:07:10.828848+00:00", + "exitCode": 3221225781, + "binaryUnchanged": true, + "workingSourceUnchanged": true, + "baselineUnchanged": true, + "stdout": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "stderr": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } +} \ No newline at end of file diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_a.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_a.arcadia new file mode 100644 index 00000000..a26875c9 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_a.arcadia @@ -0,0 +1,14 @@ +oa_activity activity_a "A - receive request" id "f0149281-4798-58d3-acff-131eed250959" { + description "Preserve engineering record activity_a."; +} + +oa_activity activity_child "A.1 - classify request" id "5a80d3af-c7ee-59eb-9680-eb80d60cb94f" { + description "Preserve engineering record activity_child."; + parent @activity_a; +} + +interaction internal "Request assessment" id "be7a39d9-3463-534e-b37b-76a74077d434" { + source @activity_a; + target @activity_b; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_b.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_b.arcadia new file mode 100644 index 00000000..871e44ff --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_b.arcadia @@ -0,0 +1,9 @@ +oa_activity activity_b "B - assess request" id "ecf076b7-e8be-5f64-8808-6a2edae5b9e2" { + description "Preserve engineering record activity_b."; +} + +interaction external "Response decision" id "927dfa82-40d2-5c5f-a525-6a884725ea1d" { + source @activity_b; + target @activity_c; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_c.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_c.arcadia new file mode 100644 index 00000000..fd53a175 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/oa/activities/activity_activity_c.arcadia @@ -0,0 +1,4 @@ +oa_activity activity_c "C - dispatch response" id "3b258d00-854d-5ddd-a057-16eedbe31714" { + description "Preserve engineering record activity_c."; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/project.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/project.arcadia new file mode 100644 index 00000000..68466805 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/project.arcadia @@ -0,0 +1,12 @@ +arcadia 7; +project multiselect_native "Multiselect native" id "c1deb118-0fb0-5692-8b80-d24339e5b6c3" { + description "Shift-select A and B; move together, retain C and engineering semantics."; + include "oa/activities/activity_activity_a.arcadia"; + include "oa/activities/activity_activity_b.arcadia"; + include "oa/activities/activity_activity_c.arcadia"; + include "sa/functions/function_sa_function.arcadia"; + include "traceability.arcadia"; + include "views/diagram_oa_other.arcadia"; + include "views/diagram_oa_view.arcadia"; + include "views/diagram_sa_view.arcadia"; +} diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/sa/functions/function_sa_function.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/sa/functions/function_sa_function.arcadia new file mode 100644 index 00000000..8573ad40 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/sa/functions/function_sa_function.arcadia @@ -0,0 +1,4 @@ +sa_function sa_function "Technical request processing" id "878a1b33-a7c2-5a68-a8b6-d8273c6354e0" { + description "Preserve engineering record sa_function."; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/traceability.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/traceability.arcadia new file mode 100644 index 00000000..d09a0915 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/traceability.arcadia @@ -0,0 +1,16 @@ +trace preserved_refinement "Reviewed request refinement" id "b42fb6e3-db90-5349-b957-faeba373ba91" { + baseline { + oa { + description "Preserve engineering record activity_a."; + name "A - receive request"; + }; + sa { + description "Preserve engineering record sa_function."; + name "Technical request processing"; + }; + }; + description "Group geometry and Hide preserve this trace and both endpoints."; + oa @activity_a; + sa @sa_function; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_oa_other.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_oa_other.arcadia new file mode 100644 index 00000000..083a7108 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_oa_other.arcadia @@ -0,0 +1,13 @@ +diagram oa_other "Other OA view - preserve" id "c4a58011-d2d7-59b1-ad58-feacef0e2d78" { + description "Selection changes placement geometry only."; +} + +node other_a id "9aa9a68b-4c26-5d21-bc35-63d3f0f311d6" { + diagram @oa_other; + element @activity_a; + position { + x 40.0; + y 60.0; + }; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_oa_view.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_oa_view.arcadia new file mode 100644 index 00000000..00cc1245 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_oa_view.arcadia @@ -0,0 +1,93 @@ +diagram oa_view "Multiselect native - OA overview" id "00000000-0000-0000-0000-000000290001" { + description "Selection changes placement geometry only."; + routes [{ + edge @internal; + label { + fraction 0.44999998807907104; + offset { + x 7.0; + y -19.0; + }; + }; + source_attachment { + offset 0.25; + side Right; + }; + target_attachment { + offset 0.699999988079071; + side Left; + }; + waypoints [{ + x 420.0; + y 145.0; + }, { + x 420.0; + y 186.0; + }]; + }, { + edge @external; + label { + fraction 0.44999998807907104; + offset { + x 7.0; + y -19.0; + }; + }; + source_attachment { + offset 0.25; + side Right; + }; + target_attachment { + offset 0.699999988079071; + side Left; + }; + waypoints [{ + x 725.0; + y 150.0; + }, { + x 725.0; + y 336.0; + }]; + }]; +} + +node node_a id "2670dd46-9fe7-5047-b13d-59817cff96dd" { + diagram @oa_view; + element @activity_a; + position { + x 80.0; + y 80.0; + }; + size { + height 260.0; + width 310.0; + }; +} + +node node_b id "70e322db-f401-58c8-a50e-2f71c61ad721" { + diagram @oa_view; + element @activity_b; + position { + x 470.0; + y 120.0; + }; +} + +node node_c id "d6b89a5a-6e2f-50f9-9d1e-993304a54583" { + diagram @oa_view; + element @activity_c; + position { + x 780.0; + y 270.0; + }; +} + +node node_child id "87f4652b-15ef-55ed-ad7e-cc5d22b7facb" { + diagram @oa_view; + element @activity_child; + position { + x 120.0; + y 210.0; + }; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_sa_view.arcadia b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_sa_view.arcadia new file mode 100644 index 00000000..d02129e4 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/source/views/diagram_sa_view.arcadia @@ -0,0 +1,14 @@ +diagram sa_view "SA engineering - preserve" id "637b9a3e-bd74-534c-9be5-4470a408729f" { + description "Selection changes placement geometry only."; + layer Sa; +} + +node sa_node id "5f1b1c24-0583-5923-a69c-d0ea99ccb18c" { + diagram @sa_view; + element @sa_function; + position { + x 100.0; + y 110.0; + }; +} + diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/started.json b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/started.json new file mode 100644 index 00000000..21535074 --- /dev/null +++ b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/started.json @@ -0,0 +1,64 @@ +{ + "start": "2026-09-23T09:07:10.546708+00:00", + "command": [ + "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\multiselect_native.exe", + "check", + "unchanged", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\fixture-2\\baseline.json", + "C:\\ar-multiselect-9e8r6t\\.local\\multiselect-native\\run-2\\snapshots\\baseline\\source\\project.arcadia" + ], + "buildReceipt": { + "bytes": 3131, + "sha256": "fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b" + }, + "binary": { + "bytes": 22162432, + "sha256": "4de47a2b62192be6a1394e654f368be19e36fb966d60d6c833a5d51d18247bb1" + }, + "helper": { + "bytes": 2494, + "sha256": "1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8" + }, + "source": { + "oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + } + }, + "baseline": { + "bytes": 8944, + "sha256": "d1278e58cd31e1d61093292eddc1d7b2cc9c8cdc7fe5dd3801de8411af8db89d" + } +} \ No newline at end of file diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/stderr.log b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/stderr.log new file mode 100644 index 00000000..e69de29b diff --git a/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/stdout.log b/harness/experiments/multiselect-20260923/incidents/run-2-baseline-runtime-environment/stdout.log new file mode 100644 index 00000000..e69de29b diff --git a/harness/experiments/multiselect-20260923/native/README.md b/harness/experiments/multiselect-20260923/native/README.md new file mode 100644 index 00000000..d5d3201e --- /dev/null +++ b/harness/experiments/multiselect-20260923/native/README.md @@ -0,0 +1,91 @@ +# Native multiselect evidence package + +This package preserves native run-1 and run-2, both closed with exit 0, empty app +stdout/stderr and all three GUI/checker/fixture unchanged flags true. Each run +contains nine positive complete-Project checker snapshots, nine source files per +snapshot and three exact source-history equality groups. Only +`views/diagram_oa_view.arcadia` differs from baseline in its move and hidden +states. These artifacts do not establish native acceptance on their own; consult +the separate [saved-view report](../../../multiselect-saved-view-20260923.md) for +action observations, usability findings and the implemented subset of issue 29. + +| Run | Source commit | Captures | Layout scope | +| --- | --- | ---: | --- | +| run-1 | `12a7352ddef5f69a144ead8219a50916233d63a9` | 27 | Before selection/status toolbar correction | +| run-2 | `f6a6010baed9e6164a1fcc7ecd58256a562827d7` | 24 | Corrected toolbar layout build | + +Run-1 GUI/checker build receipts refer to the 729-file input map +`55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb`; +run-2 refers to the distinct 729-file map +`99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c`. +The [source identity evidence](../source/README.md) ties those maps to the commits +above. The GUI binary pins differ; the recorded checker binary pins match. +Run-1 is retained as evidence of the earlier layout, not as verification of the +corrected build. + +The first run-2 baseline checker attempt failed before the successful retry after +the toolchain environment was initialized. Its unchanged raw snapshot and retry +comparison are preserved separately in the [environment incident](../incidents/README.md). +The successful retry is the baseline snapshot included in this native package; +the failed invocation is not silently reclassified or presented as a GUI failure. + +These Python 3.10+ standard-library tools never operate the GUI or rerun the model +checker. From the checkout root, validate both raw runs without writing a package: + +```text +python -B harness/experiments/multiselect-20260923/native/prepare.py --root . --runs run-1 run-2 --check-only +``` + +Omit `--check-only` to package; optionally choose `--output DIRECTORY`. The default +is this native directory. Existing package outputs are not overwritten. A missing +or unfinished run, changed recorded inputs, missing/extra fixture or snapshot +files, unmatched helper bytes, or inconsistent receipts fails preparation. + +Verify a completed package independently of the original machine: + +```text +python -B PATH_TO_PACKAGE/verify.py +``` + +Each run includes all original capture JSON/images, process/start/finish records, +generation and app streams, all nine source snapshots and checker streams/receipts, +and the history-bytes record. Its fixture includes baseline JSON, the manifest, +and canonical source files. Exact helpers are found by their recorded hashes among +the native directory's `.py`/`.js` files, so original and suffixed run-2 helpers can +coexist. Helpers are archived as data and never executed by these tools. + +Referenced GUI/checker build results, exact gate helpers and input maps are +included. The checker example is read from its pinned historical input ZIP, not +from current source. +Full build logs and large source ZIPs belong in the sibling `checks` package; +verify that package separately. This package validates build receipt, input map, +example-source and executable-pin relationships without duplicating those ZIPs. +It does not contain or execute the GUI/checker binaries. + +The verifier checks every archived byte pin and exact safe membership, clean +recorded exit and unchanged flags, process and capture lifetimes, each capture's +normalized executable path against the launched GUI, one consistent positive +window ID per run, fixture/source inventories, nine positive complete-Project +checker reports, baseline/binary/helper +relationships, and the actual archived bytes in three history equality groups. +It verifies that only `views/diagram_oa_view.arcadia` differs from baseline in the +move and hidden source states, and that baseline source bytes match the fixture. +Window identity checks join recorded metadata; the receipts do not independently +map that window handle to the recorded PID. Source models are not recompiled by +this offline Python verifier. Screenshot dimensions are recorded metadata, not a +minimum-window usability evaluation. + +`manifest.json` maps each run's original historical paths to archived run, +fixture, helper, build and checker-source members and pins their bytes. +`files.json` pins the packager, verifier, README, manifest and archive. Raw bytes +are preserved; ZIP ordering and timestamps are fixed. Historic absolute paths are +labels and are not accessed by offline verification. + +GUI actions remain separately documented parent observations. Screenshots do +not prove which inputs occurred, and the checker infers a bounded common delta +from saved geometry; it cannot validate the exact pointer destination. Source +history byte equality supports the stated saved-state relationships but does not +independently authenticate button presses, focus behavior or Undo stack structure. +Run-1 evidence does not establish the corrected toolbar's usability or any later +binary's behavior. Consult the parent report for the evaluated version, observed +limitations, run differences and the remaining full-issue acceptance work. diff --git a/harness/experiments/multiselect-20260923/native/evidence.zip b/harness/experiments/multiselect-20260923/native/evidence.zip new file mode 100644 index 00000000..c95410ce Binary files /dev/null and b/harness/experiments/multiselect-20260923/native/evidence.zip differ diff --git a/harness/experiments/multiselect-20260923/native/files.json b/harness/experiments/multiselect-20260923/native/files.json new file mode 100644 index 00000000..86f507c4 --- /dev/null +++ b/harness/experiments/multiselect-20260923/native/files.json @@ -0,0 +1,22 @@ +{ + "prepare.py": { + "bytes": 7237, + "sha256": "d9d49d3082e485e670b43010822ebd3ab7a79762b36a247dcd3bdfc6b5d7f41d" + }, + "verify.py": { + "bytes": 14703, + "sha256": "7ef5f28b6c9a43bf0361eec2d8be33a1e9f284d0ffc21b50fd09a2d082642ce3" + }, + "README.md": { + "bytes": 5418, + "sha256": "8d6fa25d8bd97c654e78b89194bd499637427ce027157d568215e13421b22c03" + }, + "manifest.json": { + "bytes": 72794, + "sha256": "de7642ad43aa881f7d984629db52ea41b62d0ebacc09260edd80a5c1fee7dd7b" + }, + "evidence.zip": { + "bytes": 4399281, + "sha256": "7dc3e0571179e0f67fefe3c73462308b79e4c7f7a09bbd805e0acf0e4faeb1cf" + } +} diff --git a/harness/experiments/multiselect-20260923/native/manifest.json b/harness/experiments/multiselect-20260923/native/manifest.json new file mode 100644 index 00000000..ee76780d --- /dev/null +++ b/harness/experiments/multiselect-20260923/native/manifest.json @@ -0,0 +1,1636 @@ +{ + "schema": 1, + "kind": "multiselect-native", + "runs": { + "run-1": { + "originalRun": "C:/ar-multiselect-9e8r6t/.local/multiselect-native/run-1", + "originalFixture": "C:/ar-multiselect-9e8r6t/.local/multiselect-native/fixture", + "helpers": { + "helper": "helpers/b3a22acab7e89f188361e6defb4b81f93c64442ddc9440f9d59e9b887104a9c8.py", + "capture": "helpers/db0257fcb416a7c14b6a08b3cc834c9fb5649955d10a72edf399657fab13cd28.js", + "snapshotHelper": "helpers/99a90177358658c694462ce598ef4908389eb2a7d6d98c6fe5423d64b0b7e2c0.py", + "history": "helpers/dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723.py" + }, + "builds": { + "gui": { + "receipt": "builds/3cdd09a859288afe71dde6fb51bcc4bca84e47ceb9cb6389d57f6722d9e23fdf/result.json", + "inputs": "builds/3cdd09a859288afe71dde6fb51bcc4bca84e47ceb9cb6389d57f6722d9e23fdf/inputs.json", + "originalRelative": ".local/group-movement-gates/windows-build-1/result.json", + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "checker": { + "receipt": "builds/35f76499ac2a49446b896932c963fd8734a5755e00e3511cedf1e584e7b17704/result.json", + "inputs": "builds/35f76499ac2a49446b896932c963fd8734a5755e00e3511cedf1e584e7b17704/inputs.json", + "originalRelative": ".local/group-movement-gates/native-build-1/result.json", + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + } + }, + "checkerSource": { + "inputPath": "crates/oa-core/examples/multiselect_native.rs", + "member": "checker-source/26fe5ab5081710fdaecb9a2a3c9be6b518700e5a578f3f930ec64aa4af236527.rs" + } + }, + "run-2": { + "originalRun": "C:/ar-multiselect-9e8r6t/.local/multiselect-native/run-2", + "originalFixture": "C:/ar-multiselect-9e8r6t/.local/multiselect-native/fixture-2", + "helpers": { + "helper": "helpers/9bc2f01933b88f767311ada5c1af4cb324cc4070fb42d72b5133c5a54bf19396.py", + "capture": "helpers/3ba4edfe7ec88a96bbfdd19d7b8c0b98791d1cc019b305c7d07e1beed85f1ff3.js", + "snapshotHelper": "helpers/1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8.py", + "history": "helpers/dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723.py" + }, + "builds": { + "gui": { + "receipt": "builds/d7b191ee07c302ffed1017374b0c5d8b86245c0f3348fbac4778ae573397b9de/result.json", + "inputs": "builds/d7b191ee07c302ffed1017374b0c5d8b86245c0f3348fbac4778ae573397b9de/inputs.json", + "originalRelative": ".local/group-movement-gates/windows-build-2/result.json", + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "checker": { + "receipt": "builds/fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b/result.json", + "inputs": "builds/fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b/inputs.json", + "originalRelative": ".local/group-movement-gates/native-build-2/result.json", + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + } + }, + "checkerSource": { + "inputPath": "crates/oa-core/examples/multiselect_native.rs", + "member": "checker-source/26fe5ab5081710fdaecb9a2a3c9be6b518700e5a578f3f930ec64aa4af236527.rs" + } + } + }, + "members": { + "builds/35f76499ac2a49446b896932c963fd8734a5755e00e3511cedf1e584e7b17704/inputs.json": { + "bytes": 121985, + "sha256": "55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb" + }, + "builds/35f76499ac2a49446b896932c963fd8734a5755e00e3511cedf1e584e7b17704/result.json": { + "bytes": 3132, + "sha256": "35f76499ac2a49446b896932c963fd8734a5755e00e3511cedf1e584e7b17704" + }, + "builds/3cdd09a859288afe71dde6fb51bcc4bca84e47ceb9cb6389d57f6722d9e23fdf/inputs.json": { + "bytes": 121985, + "sha256": "55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb" + }, + "builds/3cdd09a859288afe71dde6fb51bcc4bca84e47ceb9cb6389d57f6722d9e23fdf/result.json": { + "bytes": 3082, + "sha256": "3cdd09a859288afe71dde6fb51bcc4bca84e47ceb9cb6389d57f6722d9e23fdf" + }, + "builds/d7b191ee07c302ffed1017374b0c5d8b86245c0f3348fbac4778ae573397b9de/inputs.json": { + "bytes": 121985, + "sha256": "99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c" + }, + "builds/d7b191ee07c302ffed1017374b0c5d8b86245c0f3348fbac4778ae573397b9de/result.json": { + "bytes": 3081, + "sha256": "d7b191ee07c302ffed1017374b0c5d8b86245c0f3348fbac4778ae573397b9de" + }, + "builds/fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b/inputs.json": { + "bytes": 121985, + "sha256": "99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c" + }, + "builds/fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b/result.json": { + "bytes": 3131, + "sha256": "fbecac3b0f7913db5e2ad19043c7df167a92287833ddf3ea4f24766e86ae523b" + }, + "checker-source/26fe5ab5081710fdaecb9a2a3c9be6b518700e5a578f3f930ec64aa4af236527.rs": { + "bytes": 24234, + "sha256": "26fe5ab5081710fdaecb9a2a3c9be6b518700e5a578f3f930ec64aa4af236527" + }, + "helpers/1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8.py": { + "bytes": 2494, + "sha256": "1dbfff41f07bd750d4e550660d91ae9790d2442fbfe685dc88b7f63a47f5acb8" + }, + "helpers/3ba4edfe7ec88a96bbfdd19d7b8c0b98791d1cc019b305c7d07e1beed85f1ff3.js": { + "bytes": 1399, + "sha256": "3ba4edfe7ec88a96bbfdd19d7b8c0b98791d1cc019b305c7d07e1beed85f1ff3" + }, + "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + }, + "helpers/99a90177358658c694462ce598ef4908389eb2a7d6d98c6fe5423d64b0b7e2c0.py": { + "bytes": 2490, + "sha256": "99a90177358658c694462ce598ef4908389eb2a7d6d98c6fe5423d64b0b7e2c0" + }, + "helpers/9bc2f01933b88f767311ada5c1af4cb324cc4070fb42d72b5133c5a54bf19396.py": { + "bytes": 3412, + "sha256": "9bc2f01933b88f767311ada5c1af4cb324cc4070fb42d72b5133c5a54bf19396" + }, + "helpers/b3a22acab7e89f188361e6defb4b81f93c64442ddc9440f9d59e9b887104a9c8.py": { + "bytes": 3392, + "sha256": "b3a22acab7e89f188361e6defb4b81f93c64442ddc9440f9d59e9b887104a9c8" + }, + "helpers/db0257fcb416a7c14b6a08b3cc834c9fb5649955d10a72edf399657fab13cd28.js": { + "bytes": 1399, + "sha256": "db0257fcb416a7c14b6a08b3cc834c9fb5649955d10a72edf399657fab13cd28" + }, + "helpers/dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723.py": { + "bytes": 2192, + "sha256": "dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723" + }, + "runs/run-1/fixture/baseline.json": { + "bytes": 8944, + "sha256": "d1278e58cd31e1d61093292eddc1d7b2cc9c8cdc7fe5dd3801de8411af8db89d" + }, + "runs/run-1/fixture/manifest.json": { + "bytes": 1432, + "sha256": "b528a01b5f53b7852a7c921fba17e0159713e00bf1d55c4b0e3bb71f3caadb85" + }, + "runs/run-1/fixture/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/fixture/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/fixture/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/fixture/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/fixture/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/fixture/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/fixture/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/fixture/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-1/fixture/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/01-start-0.jpg": { + "bytes": 117018, + "sha256": "6cff4fea33bf9c6b80a5a76356358bfe9f7e1740151e3607684cfc71a4d13ed2" + }, + "runs/run-1/raw/01-start.json": { + "bytes": 918, + "sha256": "9c0198997798a361c51a130366ac5de8437f8584bf0f2e061b91e8e2dc4cd705" + }, + "runs/run-1/raw/02-open-dialog-0.jpg": { + "bytes": 115734, + "sha256": "8976086dfd728c8d177f0bb35ec11ffa227add4e7eedf135866ca957f2b32b7c" + }, + "runs/run-1/raw/02-open-dialog.json": { + "bytes": 924, + "sha256": "51da71a314ec3aa3f17eaf18d658bcd01e0385d715da5aa6eb68e5982c82d8f4" + }, + "runs/run-1/raw/03-loaded-0.jpg": { + "bytes": 94058, + "sha256": "d25b60203dea99ecab9bc1686b29d9df1965fea4378409c92d16481fadb8e2c5" + }, + "runs/run-1/raw/03-loaded.json": { + "bytes": 723, + "sha256": "94e89b78105973d704e53e2945e96ceaaa56178fdb546acfe32f07f41d62db60" + }, + "runs/run-1/raw/04-open-settled-0.jpg": { + "bytes": 94058, + "sha256": "d25b60203dea99ecab9bc1686b29d9df1965fea4378409c92d16481fadb8e2c5" + }, + "runs/run-1/raw/04-open-settled.json": { + "bytes": 729, + "sha256": "463d3b0e8da50eb1ec6b94cd25de80fd568c4ecab8f9d6afcaa0adb7a6a6689f" + }, + "runs/run-1/raw/05-project-open-0.jpg": { + "bytes": 113024, + "sha256": "3dea0a7788a60f7251fcaf8dedc5a3be1b1c5f538e04056441c1a0c7516afee8" + }, + "runs/run-1/raw/05-project-open.json": { + "bytes": 933, + "sha256": "5a71a99e22a95b58bfe3c4c683947144cc1fd3eaeb9bde07a158643cc9f1d82a" + }, + "runs/run-1/raw/06-select-a-0.jpg": { + "bytes": 118333, + "sha256": "0683581fcc0f1bab6b03167e31fb455fa49b7b49f2ce9f38af7e3bd07ff02eb6" + }, + "runs/run-1/raw/06-select-a.json": { + "bytes": 931, + "sha256": "8869c10294896e1d3291cef7482f8e4fdc76e8ed05f0ea407ab3ef3a31b39db6" + }, + "runs/run-1/raw/07-b-context-0.jpg": { + "bytes": 131371, + "sha256": "e10408c30c76569cdf5f30ff75f5118eac638ed1eca0d69ab1cf118b49229676" + }, + "runs/run-1/raw/07-b-context.json": { + "bytes": 932, + "sha256": "98da8c0b28c3a297b90191c13fa05663d49bd6a2471ee5eb5f1c96d744e4ec53" + }, + "runs/run-1/raw/08-group-selected-0.jpg": { + "bytes": 121401, + "sha256": "4cba3326ebce60382793cde859079f66c111dbcbf8892ea82932d634eac49dca" + }, + "runs/run-1/raw/08-group-selected.json": { + "bytes": 937, + "sha256": "084ceadf32acad6d7040cab3f9c11b6a22c68cb70e2ff84fb828bbc632081f8c" + }, + "runs/run-1/raw/09-group-moved-0.jpg": { + "bytes": 121707, + "sha256": "91fe107932ab3d42c202ac98650537965596bb19cf1a3207e5d9f026e56a9189" + }, + "runs/run-1/raw/09-group-moved.json": { + "bytes": 944, + "sha256": "c5dfaa2f687010fbd32c5dafb404caa22433386e74a7d32fad210003c2c65eae" + }, + "runs/run-1/raw/10-move-saved-0.jpg": { + "bytes": 120345, + "sha256": "28b041ac1754ab822cc54697ad6d25c692c3867ee1090c542760b8e43142470c" + }, + "runs/run-1/raw/10-move-saved.json": { + "bytes": 933, + "sha256": "6e4e5150ab14769434607da64034db836e0352a1b6d40505c874ddd17594d132" + }, + "runs/run-1/raw/11-move-undo-0.jpg": { + "bytes": 121126, + "sha256": "d51e390b6d49b72b860b2ac5dc2218b6d20cbc7ee3298ac985e67f4a8b3af92c" + }, + "runs/run-1/raw/11-move-undo.json": { + "bytes": 934, + "sha256": "4713f556a6dfa6d293269f33df33185e403503b3db1d1452801b800bb7dbe9ac" + }, + "runs/run-1/raw/12-move-undo-saved-0.jpg": { + "bytes": 120839, + "sha256": "b40ff80d5cf4ee93bc87b7232db4e87983486cac5ed036c9fd1fc7f970233d20" + }, + "runs/run-1/raw/12-move-undo-saved.json": { + "bytes": 940, + "sha256": "5c2cb43cb5a14d731d3ce1583f8ada6f937519aedd65290bea973c00c8400d35" + }, + "runs/run-1/raw/13-move-redo-0.jpg": { + "bytes": 120588, + "sha256": "d683584773c21c766bfec579d6f8c0614dff0cfdab93c50bf20b8a58ce6b8936" + }, + "runs/run-1/raw/13-move-redo.json": { + "bytes": 942, + "sha256": "2ff1611aa83da626780614de69c2f3c6dc2a0df206427ecd3b1033329733b929" + }, + "runs/run-1/raw/14-move-redo-saved-0.jpg": { + "bytes": 120355, + "sha256": "0f47cfbec53cad0716218b9e144ce01575b6a47c36de01fce8318a3b457c86fd" + }, + "runs/run-1/raw/14-move-redo-saved.json": { + "bytes": 940, + "sha256": "5e9065ec5c52de43cddc1d0ca838b8958d673defdaecbba5fb5921f1035fe0ce" + }, + "runs/run-1/raw/15-before-hide-undo-0.jpg": { + "bytes": 121119, + "sha256": "c1d52b30d4379f3629d9275752710ff5754ca140581041bcd8cbf35cd36594f0" + }, + "runs/run-1/raw/15-before-hide-undo.json": { + "bytes": 949, + "sha256": "88b359d28a83a4a71183c209ba80c51616391d5e7eb5f2812c535a3e5ec8f6b6" + }, + "runs/run-1/raw/16-before-hide-saved-0.jpg": { + "bytes": 120810, + "sha256": "2a16980fd6725b485e75084744530d01a3a037339a649e92db0f2473b7340474" + }, + "runs/run-1/raw/16-before-hide-saved.json": { + "bytes": 942, + "sha256": "a6de79cc88ce37ceb31d149273fdefaea54b44b626e149349b7d35eff6544970" + }, + "runs/run-1/raw/17-group-hidden-0.jpg": { + "bytes": 120737, + "sha256": "a6f60f1d123e615d7585e22452888cba4cf0fac7d15f481aa906dba7ab2eaad4" + }, + "runs/run-1/raw/17-group-hidden.json": { + "bytes": 935, + "sha256": "d2ce4bcc9f2bff82ed2783cc909148f3b0dd988c8a199e39d0fae051a9d360bd" + }, + "runs/run-1/raw/18-hide-settled-0.jpg": { + "bytes": 116357, + "sha256": "993fa89b32579e67b4a943a16c5e1612d87a9d1eadaa4a75c228392ccf8da4d1" + }, + "runs/run-1/raw/18-hide-settled.json": { + "bytes": 945, + "sha256": "6f8ce9db351a671817a598f133dc450a98f7c4557e5473a988aecd56250d8bf3" + }, + "runs/run-1/raw/19-hide-saved-0.jpg": { + "bytes": 115366, + "sha256": "45ecdb44ae21d855897647fa454cf9c244f178097d88b18eedd40e85cf18d89c" + }, + "runs/run-1/raw/19-hide-saved.json": { + "bytes": 935, + "sha256": "70671ed58a4154d3abfcc58f32e876f7474989840f0924a8552150e8412f15be" + }, + "runs/run-1/raw/20-hide-undo-0.jpg": { + "bytes": 115614, + "sha256": "b28c9e1d3832dfa6aeed96793d0184129ae44ae77cfea7b8067a3b29e8c03b56" + }, + "runs/run-1/raw/20-hide-undo.json": { + "bytes": 934, + "sha256": "ba779cb37262339a3ea19530c54f84f8fddd172a152f03bdb13217ee38ed1cdc" + }, + "runs/run-1/raw/21-hide-undo-settled-0.jpg": { + "bytes": 122738, + "sha256": "c9adeabcefae131a7930414d491a0947b728eadd7f9bc0b50130006846f57c09" + }, + "runs/run-1/raw/21-hide-undo-settled.json": { + "bytes": 950, + "sha256": "6adf9cebc12dd0fdb8737dff449b83dd13fd3506ced7fdae01cad00f9abe5714" + }, + "runs/run-1/raw/22-hide-undo-saved-0.jpg": { + "bytes": 122465, + "sha256": "f396020e20c1c29bffa6b270cd518e3d9b159196919b8d0069cdf3529c610c08" + }, + "runs/run-1/raw/22-hide-undo-saved.json": { + "bytes": 938, + "sha256": "b883b9b9d6e6a05f2c834d2bd607994d38c267c623f6e0c8ae499c261c4b1c53" + }, + "runs/run-1/raw/23-hide-redo-0.jpg": { + "bytes": 115576, + "sha256": "490d67cdd98c3f09bfafab153c3dd3e8a886dff520901361dd8f4f8e09e8d121" + }, + "runs/run-1/raw/23-hide-redo.json": { + "bytes": 942, + "sha256": "2068137772446b986677f110082380dba3bcbcec0a42fd9c68ad2f7dd598f784" + }, + "runs/run-1/raw/24-hide-redo-saved-0.jpg": { + "bytes": 115405, + "sha256": "9ded1378d102c7bdbadfc52f3ac716afb6732a81447ac26a5300c2be924691af" + }, + "runs/run-1/raw/24-hide-redo-saved.json": { + "bytes": 938, + "sha256": "0e2d49dd5e8f356c916b7f05460039c00c1295541ea7e56178ef68f66f92a1ed" + }, + "runs/run-1/raw/25-reopen-request-0.jpg": { + "bytes": 101295, + "sha256": "a24f52dc838566f7f02ceb05ae900b46eed870c4698c8a59b3c076dfee3026c5" + }, + "runs/run-1/raw/25-reopen-request.json": { + "bytes": 417, + "sha256": "54a75ae96af98d1a0d22b0c529e6d6a4e91aaa164547c1da5655df8e774324f8" + }, + "runs/run-1/raw/26-reopened-hidden-0.jpg": { + "bytes": 105465, + "sha256": "899b64e11b665faf92f034d2475cb1701d8b6a1e838f21cb38c120a33d885249" + }, + "runs/run-1/raw/26-reopened-hidden.json": { + "bytes": 938, + "sha256": "649814bc4409f68f8419aea2a8de7b80034e561834a09943e1b4da4e6fb4bbf5" + }, + "runs/run-1/raw/27-reopened-saved-0.jpg": { + "bytes": 103742, + "sha256": "059600b57fdf90fbc96229f24ecbbcb21008f953172c37b4c682e3879625df73" + }, + "runs/run-1/raw/27-reopened-saved.json": { + "bytes": 937, + "sha256": "c1168c86d8473921b0e117e6d1b561e73ff3da9d4b5fda1683ae4d5753eda916" + }, + "runs/run-1/raw/app.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/app.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/finished.json": { + "bytes": 393, + "sha256": "d7ef4be0f8c862be9621f4c2fcc41d92b8a366bc8d938c6f0a13cb3cbaf87749" + }, + "runs/run-1/raw/generation.json": { + "bytes": 1245, + "sha256": "f29aca035de2ac20c69351a0dd6f43c6081d1961167cc8dc8d734e2cefbe71c9" + }, + "runs/run-1/raw/generation.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/generation.stdout.log": { + "bytes": 98, + "sha256": "22d09868a6dafc631d88726197ec90c07056096383420b5727ed873a84b8afa6" + }, + "runs/run-1/raw/history-bytes.json": { + "bytes": 780, + "sha256": "01eda6a567412f547d31645a748a9f0e40236050a0b231fe0ee0a035ca82cc9a" + }, + "runs/run-1/raw/processes.json": { + "bytes": 233, + "sha256": "4f6d4b8c952d328f5058bf0b5e7c5224c093bca72d3a9d955a799efa847af13b" + }, + "runs/run-1/raw/snapshots/baseline/result.json": { + "bytes": 2756, + "sha256": "abfbeb5b5c8548b20a26db61ca381e2bd40da51ae3d5ab6a00ca986f7b74b163" + }, + "runs/run-1/raw/snapshots/baseline/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/baseline/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/baseline/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/baseline/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/baseline/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/baseline/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/baseline/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/baseline/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-1/raw/snapshots/baseline/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/baseline/started.json": { + "bytes": 2354, + "sha256": "4aae4aa4b46bbc94ab0c7df6902601411d6af80f8461b77926bb060359c0b9b7" + }, + "runs/run-1/raw/snapshots/baseline/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/baseline/stdout.log": { + "bytes": 439, + "sha256": "769684fc08d023505c76c237937370e1b7c7b4e3ee2737e59b55eb69c7051505" + }, + "runs/run-1/raw/snapshots/before-hide/result.json": { + "bytes": 2759, + "sha256": "88dadd5d737575079e83c590c6bd7691f63ef5e222f5ded56658c222b49e832a" + }, + "runs/run-1/raw/snapshots/before-hide/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/before-hide/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/before-hide/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/before-hide/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/before-hide/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/before-hide/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/before-hide/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/before-hide/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-1/raw/snapshots/before-hide/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/before-hide/started.json": { + "bytes": 2357, + "sha256": "1e691493de565f339d27d798818fc953114a09c70dd77a9b55e55e3271443c79" + }, + "runs/run-1/raw/snapshots/before-hide/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/before-hide/stdout.log": { + "bytes": 442, + "sha256": "138facd3f4d620efdb36417c66c6e5f854cacb5d3b5640dfe5300fd8f8b8dbe0" + }, + "runs/run-1/raw/snapshots/hidden/result.json": { + "bytes": 2749, + "sha256": "74da61d67a53c9d6f26f1e3b5cd91200e54a55419306cd37f2ea09de7242c6df" + }, + "runs/run-1/raw/snapshots/hidden/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/hidden/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/hidden/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/hidden/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/hidden/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/hidden/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/hidden/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/hidden/source/views/diagram_oa_view.arcadia": { + "bytes": 1652, + "sha256": "f2ee5975b2335415c1a6214e7eeccfc97715c53fdce617ddab595ba2acb087a1" + }, + "runs/run-1/raw/snapshots/hidden/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/hidden/started.json": { + "bytes": 2347, + "sha256": "fcb637ce5751ce779a8ba92c30d5b190b07e8b30cbc9e627f0dbccb71ba653a2" + }, + "runs/run-1/raw/snapshots/hidden/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/hidden/stdout.log": { + "bytes": 432, + "sha256": "8361156e746393c7599a6516bfb3dea93895fe6726736cec75638cf5ac56529f" + }, + "runs/run-1/raw/snapshots/hide-redo/result.json": { + "bytes": 2752, + "sha256": "18d7d6caf7739e7c5a299e592c6eb4afe9d710fe1f7c8e3d3eb6cc692a0cfae2" + }, + "runs/run-1/raw/snapshots/hide-redo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/hide-redo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/hide-redo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/hide-redo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/hide-redo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/hide-redo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/hide-redo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/hide-redo/source/views/diagram_oa_view.arcadia": { + "bytes": 1652, + "sha256": "f2ee5975b2335415c1a6214e7eeccfc97715c53fdce617ddab595ba2acb087a1" + }, + "runs/run-1/raw/snapshots/hide-redo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/hide-redo/started.json": { + "bytes": 2350, + "sha256": "89e981fddf8c132b3fff21651c69033e41d8ba4510356c18b131e7d31dd83c85" + }, + "runs/run-1/raw/snapshots/hide-redo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/hide-redo/stdout.log": { + "bytes": 435, + "sha256": "8ddd7948493da0d2dcc49d3e38b85a1a96bca7f9d307f4c9c3e0bdc48285dd26" + }, + "runs/run-1/raw/snapshots/hide-undo/result.json": { + "bytes": 2757, + "sha256": "7222322a1ea921f61ed39d0da4a800028192ad331fd323f2d9e4c2ffc0b0189e" + }, + "runs/run-1/raw/snapshots/hide-undo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/hide-undo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/hide-undo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/hide-undo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/hide-undo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/hide-undo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/hide-undo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/hide-undo/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-1/raw/snapshots/hide-undo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/hide-undo/started.json": { + "bytes": 2355, + "sha256": "17e859661822e83e9ff66f78fce4a52cea4c78edb1bdf1ade1a2343d3d8d5aeb" + }, + "runs/run-1/raw/snapshots/hide-undo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/hide-undo/stdout.log": { + "bytes": 440, + "sha256": "23a539b6904bd2ca347a8ff08b208b210c60f687f2d896b570bef6fb170d0476" + }, + "runs/run-1/raw/snapshots/move-redo/result.json": { + "bytes": 2753, + "sha256": "61846bc4bf742e125819bf265b0a857467776fa56fe39be92ae90aca58ec9cc5" + }, + "runs/run-1/raw/snapshots/move-redo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/move-redo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/move-redo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/move-redo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/move-redo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/move-redo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/move-redo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/move-redo/source/views/diagram_oa_view.arcadia": { + "bytes": 1898, + "sha256": "204c6a537e51c8040787ad1ac5abb5f0b245c4e4b07575603363ad308d0889ca" + }, + "runs/run-1/raw/snapshots/move-redo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/move-redo/started.json": { + "bytes": 2351, + "sha256": "fcf99b461e87328e1eccffed6a73ab103c858016961aa17c7e1b336bd5169e5b" + }, + "runs/run-1/raw/snapshots/move-redo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/move-redo/stdout.log": { + "bytes": 467, + "sha256": "416d64479efbc659d03186aa5735c2044590811625a470e423678f7d9b28e75a" + }, + "runs/run-1/raw/snapshots/move-undo/result.json": { + "bytes": 2757, + "sha256": "1690db396d9034248ae0bc1504c123b6416f43b8feccea1accecd14bdb2e6562" + }, + "runs/run-1/raw/snapshots/move-undo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/move-undo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/move-undo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/move-undo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/move-undo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/move-undo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/move-undo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/move-undo/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-1/raw/snapshots/move-undo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/move-undo/started.json": { + "bytes": 2355, + "sha256": "837ca43e4113e04da3074944521e9d0762950e3259b16e8645e148eb1c968376" + }, + "runs/run-1/raw/snapshots/move-undo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/move-undo/stdout.log": { + "bytes": 440, + "sha256": "92c91ff39e0967add8b0750bbcc289dd2f8c588668793e3c67c75d889ad2419d" + }, + "runs/run-1/raw/snapshots/move/result.json": { + "bytes": 2748, + "sha256": "d9a36169143ec156992be2893c612f9e6dc800d224a46e107254b7064a3808f7" + }, + "runs/run-1/raw/snapshots/move/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/move/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/move/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/move/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/move/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/move/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/move/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/move/source/views/diagram_oa_view.arcadia": { + "bytes": 1898, + "sha256": "204c6a537e51c8040787ad1ac5abb5f0b245c4e4b07575603363ad308d0889ca" + }, + "runs/run-1/raw/snapshots/move/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/move/started.json": { + "bytes": 2346, + "sha256": "954d0e61f1a7a87fe053f4d63abbd0bb084d83669e234039033f7cd59a0a5e2c" + }, + "runs/run-1/raw/snapshots/move/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/move/stdout.log": { + "bytes": 462, + "sha256": "7a9f44bc3b3916a6ee00b37cd23eac858662b87009be2932608b661c89619de4" + }, + "runs/run-1/raw/snapshots/reopened-hidden/result.json": { + "bytes": 2758, + "sha256": "54a1bc181f0fb779c9d912ed76a22c702713f72e6d52e048631eb427c9837a94" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/views/diagram_oa_view.arcadia": { + "bytes": 1652, + "sha256": "f2ee5975b2335415c1a6214e7eeccfc97715c53fdce617ddab595ba2acb087a1" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-1/raw/snapshots/reopened-hidden/started.json": { + "bytes": 2356, + "sha256": "9877f8c2e9cb11ded86649f1cb52d8100516f7865365f9a16e718f286699ee14" + }, + "runs/run-1/raw/snapshots/reopened-hidden/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/reopened-hidden/stdout.log": { + "bytes": 441, + "sha256": "4f38dd34005b12910ebcee5350e2e944a2d1bbbf8ea7ed1db550df7f3ab2aae1" + }, + "runs/run-1/raw/started.json": { + "bytes": 4016, + "sha256": "d2fc17c40d0e7f0ca1d2cfbb051de7613feb4e596944693060ea5c96bca43079" + }, + "runs/run-2/fixture/baseline.json": { + "bytes": 8944, + "sha256": "d1278e58cd31e1d61093292eddc1d7b2cc9c8cdc7fe5dd3801de8411af8db89d" + }, + "runs/run-2/fixture/manifest.json": { + "bytes": 1432, + "sha256": "b528a01b5f53b7852a7c921fba17e0159713e00bf1d55c4b0e3bb71f3caadb85" + }, + "runs/run-2/fixture/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/fixture/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/fixture/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/fixture/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/fixture/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/fixture/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/fixture/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/fixture/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-2/fixture/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/01-start-0.jpg": { + "bytes": 115994, + "sha256": "01fc320d71d7a12847f47006183d04633abf97dd0da8c3c722ad8e2af4f35208" + }, + "runs/run-2/raw/01-start.json": { + "bytes": 920, + "sha256": "045018b64e4c21e684ac445bf4882a097626aaa09a0fe8da76b25f1826107029" + }, + "runs/run-2/raw/02-open-requested-0.jpg": { + "bytes": 114601, + "sha256": "b96e88b95e320f570b20d341f5f698a1da275d0ffe9f347d22c98c5ae352fdcc" + }, + "runs/run-2/raw/02-open-requested.json": { + "bytes": 929, + "sha256": "236367606aa07fe73abdeeb5b080c8d5e6c3763a0cf84f8533cad490f2dad967" + }, + "runs/run-2/raw/03-open-submitted-0.jpg": { + "bytes": 113844, + "sha256": "f8d0e62e15ff3e4444f3e6b302bcdd33c3e47a3a3912928bdca24e86bae60ddb" + }, + "runs/run-2/raw/03-open-submitted.json": { + "bytes": 937, + "sha256": "a8e783480d1f3965a966b5edb2f1f764d1b11f8a0fccf5d209e9801f545f7992" + }, + "runs/run-2/raw/04-select-a-0.jpg": { + "bytes": 120054, + "sha256": "a59569599f569bd061d3e9192f3557edfab6f231fc79fe2824fbc98478d7ce3b" + }, + "runs/run-2/raw/04-select-a.json": { + "bytes": 933, + "sha256": "0055487b4c7783d1c5b8638bb2e53a46dafce139888d9d54d30d9ad27fab6945" + }, + "runs/run-2/raw/05-singleton-settled-0.jpg": { + "bytes": 120676, + "sha256": "5b95b09349edd32b913662d4d86698325b9229a09ec1e584f0ff23524413ccf6" + }, + "runs/run-2/raw/05-singleton-settled.json": { + "bytes": 942, + "sha256": "9b7dca5e3533fb58948f1ea227b36498808e69ae759897f3710b9938685db49b" + }, + "runs/run-2/raw/06-add-menu-0.jpg": { + "bytes": 133327, + "sha256": "f9c9949d0bfd3ea37f2d3e41024b99bc1e32ac1b24f87317183872397b7f34fd" + }, + "runs/run-2/raw/06-add-menu.json": { + "bytes": 933, + "sha256": "dc42f584e4fd88a56b6cfc796559bd8c47a959e0f0a78fff75648e8809858090" + }, + "runs/run-2/raw/07-group-selected-0.jpg": { + "bytes": 122725, + "sha256": "d64c48fc44dd83b30e3ff1832d8cae80070018a6601a24a8a7d5cbc8a88128a5" + }, + "runs/run-2/raw/07-group-selected.json": { + "bytes": 939, + "sha256": "c805b943fd1b28ad4bcee7449e8b8ae10db03184338cfbdcebc5d82cac4fb324" + }, + "runs/run-2/raw/08-group-drag-0.jpg": { + "bytes": 124333, + "sha256": "b242aab133480834eda58ef5cfef0bd9a6f8f16de838e38f79c599865d4b859d" + }, + "runs/run-2/raw/08-group-drag.json": { + "bytes": 945, + "sha256": "ab6f1cea07447916fc51a78b219733f1dac6108429c94b9b02c48db85a2af63f" + }, + "runs/run-2/raw/09-move-saved-0.jpg": { + "bytes": 122837, + "sha256": "49937f02495453f61e2b9c7e6f396f4e69b25ce541f862f4d1edcd2b65972878" + }, + "runs/run-2/raw/09-move-saved.json": { + "bytes": 935, + "sha256": "c758050005c3602ea427b2cdb3bf82035cc45522ba88b4bd707f4b5466e46dac" + }, + "runs/run-2/raw/10-move-undo-0.jpg": { + "bytes": 121793, + "sha256": "cd44dfedcb2473c77f44fdda0c5a7b4294a23ea872e9dfcd9c60aaef72583b66" + }, + "runs/run-2/raw/10-move-undo.json": { + "bytes": 944, + "sha256": "dbc2ded072ae5450f0c4e44b468c1af6dd3a2383e216dfd5b24299caa4f021ed" + }, + "runs/run-2/raw/11-move-undo-saved-0.jpg": { + "bytes": 121824, + "sha256": "b24e9f4af258e39446242624c84e5764e3e652210f5c75b4d49a4156516b3adb" + }, + "runs/run-2/raw/11-move-undo-saved.json": { + "bytes": 940, + "sha256": "ea32e30b98a2ba75a1c4264b3f68c6290d03697621e14475f95af68d2d6ef5b6" + }, + "runs/run-2/raw/12-move-redo-0.jpg": { + "bytes": 122787, + "sha256": "de4de547beaacdc633dccc00b633db82a4faedfcfe0e7f240e81af40c630f1bd" + }, + "runs/run-2/raw/12-move-redo.json": { + "bytes": 944, + "sha256": "3619cccaf18a44f9d1883021e763b93e5ea9646592f4219bab64baf05ddcae85" + }, + "runs/run-2/raw/13-move-redo-saved-0.jpg": { + "bytes": 122837, + "sha256": "49937f02495453f61e2b9c7e6f396f4e69b25ce541f862f4d1edcd2b65972878" + }, + "runs/run-2/raw/13-move-redo-saved.json": { + "bytes": 940, + "sha256": "7a7dd22eb1cb0e78a453f5519d17ee9c30f1cec64712cebaf121c42e7d2dd5f3" + }, + "runs/run-2/raw/14-before-hide-undo-0.jpg": { + "bytes": 121793, + "sha256": "cd44dfedcb2473c77f44fdda0c5a7b4294a23ea872e9dfcd9c60aaef72583b66" + }, + "runs/run-2/raw/14-before-hide-undo.json": { + "bytes": 951, + "sha256": "cf80c4c8403cbcc69d42a930748e0cb5f8d5b938ad22a5c8dbc5787a4e02bf5e" + }, + "runs/run-2/raw/15-before-hide-saved-0.jpg": { + "bytes": 121646, + "sha256": "f7780ee64a1d80fde3dfcf3e4e0b738c8ae4d267b1655be92151beb2e8572e9b" + }, + "runs/run-2/raw/15-before-hide-saved.json": { + "bytes": 944, + "sha256": "6e03b09c7375655c13a3b8a0a3649204d05ec2e3b172f594a5815543b4014f29" + }, + "runs/run-2/raw/16-hide-group-0.jpg": { + "bytes": 117680, + "sha256": "4e66045e4cdecb8159a728c8fc412158bdac6a535814341c5b483e23d6191f6a" + }, + "runs/run-2/raw/16-hide-group.json": { + "bytes": 945, + "sha256": "af6b0431db1bb1f9c9e981534eebb2379eef3c938a35ba8c4a2e3498b3ce46bc" + }, + "runs/run-2/raw/17-hide-saved-0.jpg": { + "bytes": 116977, + "sha256": "576d5570eb8294bcf0df9fc9f1bf94909d940de10b53638aff8ab169987e6da1" + }, + "runs/run-2/raw/17-hide-saved.json": { + "bytes": 937, + "sha256": "c8a8c7552fabf674f3f1e39910d2b32515166b5638b33375c0e10d9124239337" + }, + "runs/run-2/raw/18-hide-undo-0.jpg": { + "bytes": 123909, + "sha256": "ebfa7ed0e08a94ec4797c85059243625d4bf6a426128d22588e01ca7e93589b2" + }, + "runs/run-2/raw/18-hide-undo.json": { + "bytes": 944, + "sha256": "23865b4b2f365a6762337acd211e1339e9b5fd3ea69c4471a6f00dfcfae914c0" + }, + "runs/run-2/raw/19-hide-undo-saved-0.jpg": { + "bytes": 123927, + "sha256": "7530d117a26bda7f91e06d7cb8990db8ad98e396bbc49918ccf58fad24cc51ae" + }, + "runs/run-2/raw/19-hide-undo-saved.json": { + "bytes": 942, + "sha256": "c0b591e636da6be80fef38ab62cc6968ee63d4ac0eb75e87811b3264c258b773" + }, + "runs/run-2/raw/20-hide-redo-0.jpg": { + "bytes": 116913, + "sha256": "f038932d4629503648c6a71130101510b8887dba9a2cd14e5453d85ed160559c" + }, + "runs/run-2/raw/20-hide-redo.json": { + "bytes": 944, + "sha256": "052d22c743afc53506bece7fca2694661282025e1eac24e4acba764df32ceedf" + }, + "runs/run-2/raw/21-hide-redo-saved-0.jpg": { + "bytes": 116977, + "sha256": "576d5570eb8294bcf0df9fc9f1bf94909d940de10b53638aff8ab169987e6da1" + }, + "runs/run-2/raw/21-hide-redo-saved.json": { + "bytes": 942, + "sha256": "1084637fc5cc252fe8a71576edc121b5dbd1a51ef75aad99a4682fb9fbc61869" + }, + "runs/run-2/raw/22-reopen-requested-0.jpg": { + "bytes": 100060, + "sha256": "518d18e59ff673622a6912f3eda0f2728ec3d82be0e61571467767fc40eb77b9" + }, + "runs/run-2/raw/22-reopen-requested.json": { + "bytes": 941, + "sha256": "9a177ecf92c49debfef553945a60082954b823eaa0409ffe1123d3c4095e9037" + }, + "runs/run-2/raw/23-reopened-hidden-0.jpg": { + "bytes": 106265, + "sha256": "302e95f29e44bb7f0fa9293fbe642fda6dd15951c965d92699d532ab5a1abe08" + }, + "runs/run-2/raw/23-reopened-hidden.json": { + "bytes": 940, + "sha256": "ae4dbd8c9c02b1d8c1ce463e9e513e6266e038e58e3a950cb51c19d43611585a" + }, + "runs/run-2/raw/24-reopened-saved-0.jpg": { + "bytes": 104545, + "sha256": "494dfc4cb8119fa7d06a628fbefd32be4ef47b1bfc636b59a507decb7b2dd2bf" + }, + "runs/run-2/raw/24-reopened-saved.json": { + "bytes": 939, + "sha256": "e35dd5bad10107f043a892a8fd19e49c45cbf25c04f2d8ef716b4fcc039743cb" + }, + "runs/run-2/raw/app.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/app.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/finished.json": { + "bytes": 393, + "sha256": "1d70c300365cf71d008aee5552a9bbd1c6494127b4fd139aa25a5181b70e956f" + }, + "runs/run-2/raw/generation.json": { + "bytes": 1248, + "sha256": "005a7e124100de77d865ea70df2867476a3e2374db7e71fea6f8d98bb23896a1" + }, + "runs/run-2/raw/generation.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/generation.stdout.log": { + "bytes": 100, + "sha256": "1d02781c76fac03e3e34372d7c230233a16fba4ec9d29eb623246d87b57ad774" + }, + "runs/run-2/raw/history-bytes.json": { + "bytes": 780, + "sha256": "9087e0423fe6fbd0f8dfb368e092a44ad8388041e753fcfe1406bd1707b1f776" + }, + "runs/run-2/raw/processes.json": { + "bytes": 233, + "sha256": "ab68d6c00ab403ff950a2214d4f37113a1a99cc1021da461762efe4fcf80d5c6" + }, + "runs/run-2/raw/snapshots/baseline/result.json": { + "bytes": 2758, + "sha256": "ae16cbc54bf5b6054750ef082f47bf0a26ee2dfb7bce46f19c8fd2c1ac622d7f" + }, + "runs/run-2/raw/snapshots/baseline/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/baseline/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/baseline/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/baseline/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/baseline/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/baseline/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/baseline/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/baseline/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-2/raw/snapshots/baseline/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/baseline/started.json": { + "bytes": 2356, + "sha256": "e172a36f0872d49a8fe0f7200fb56d597b71fd1849dc9902d53ac1813a3c7d4a" + }, + "runs/run-2/raw/snapshots/baseline/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/baseline/stdout.log": { + "bytes": 441, + "sha256": "12842cc4e483e2546f5c8615b17fe72c5fe07f3fb4440469071433c82d9a9b30" + }, + "runs/run-2/raw/snapshots/before-hide/result.json": { + "bytes": 2761, + "sha256": "1bd192a74bbff1c9755e029ff4723233e25b7838fb810167e8619162f5448e35" + }, + "runs/run-2/raw/snapshots/before-hide/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/before-hide/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/before-hide/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/before-hide/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/before-hide/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/before-hide/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/before-hide/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/before-hide/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-2/raw/snapshots/before-hide/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/before-hide/started.json": { + "bytes": 2359, + "sha256": "c5ec430694cd8d7fe2061475966c3d6706bc10da7aada9df52ca5b701a28c5b2" + }, + "runs/run-2/raw/snapshots/before-hide/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/before-hide/stdout.log": { + "bytes": 444, + "sha256": "3d0d50b67b5351cf4312436a3e47f1904c64227ddc0c533e1302ffe9d2b9483e" + }, + "runs/run-2/raw/snapshots/hidden/result.json": { + "bytes": 2751, + "sha256": "e81411c2921e8f347684e9e273119922d001b003cc1391f32f980eccba14b2a8" + }, + "runs/run-2/raw/snapshots/hidden/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/hidden/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/hidden/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/hidden/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/hidden/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/hidden/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/hidden/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/hidden/source/views/diagram_oa_view.arcadia": { + "bytes": 1652, + "sha256": "f2ee5975b2335415c1a6214e7eeccfc97715c53fdce617ddab595ba2acb087a1" + }, + "runs/run-2/raw/snapshots/hidden/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/hidden/started.json": { + "bytes": 2349, + "sha256": "5d40939cf1e9d86c9e188c698b548aef5822b04a14993038be37df3d3d8446da" + }, + "runs/run-2/raw/snapshots/hidden/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/hidden/stdout.log": { + "bytes": 434, + "sha256": "d645027a7d35320bbeaee70e9511408aaeb48838368ca964aac98f572a2bcf0c" + }, + "runs/run-2/raw/snapshots/hide-redo/result.json": { + "bytes": 2754, + "sha256": "6695f0aed81944c66ffa2b95d2d507874c008aa5553924590ecbed4decd587ce" + }, + "runs/run-2/raw/snapshots/hide-redo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/hide-redo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/hide-redo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/hide-redo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/hide-redo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/hide-redo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/hide-redo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/hide-redo/source/views/diagram_oa_view.arcadia": { + "bytes": 1652, + "sha256": "f2ee5975b2335415c1a6214e7eeccfc97715c53fdce617ddab595ba2acb087a1" + }, + "runs/run-2/raw/snapshots/hide-redo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/hide-redo/started.json": { + "bytes": 2352, + "sha256": "72a4ebf593c6f98de53ed4801d24638305eb23fe7b30eb98945de72b35bf9d85" + }, + "runs/run-2/raw/snapshots/hide-redo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/hide-redo/stdout.log": { + "bytes": 437, + "sha256": "4842e451ac84f55d267de262fa8d7489abdbdbe82405995fd1dc4c5a08bbceca" + }, + "runs/run-2/raw/snapshots/hide-undo/result.json": { + "bytes": 2759, + "sha256": "3aad11fb2d2d8360cef5fd4660bd7621996ed4678ea064c7aca9908890e921df" + }, + "runs/run-2/raw/snapshots/hide-undo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/hide-undo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/hide-undo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/hide-undo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/hide-undo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/hide-undo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/hide-undo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/hide-undo/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-2/raw/snapshots/hide-undo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/hide-undo/started.json": { + "bytes": 2357, + "sha256": "ca62d7070ec4cf198d147eae44f81b4cfad86bd53ddd5a0e262132cba8ba5661" + }, + "runs/run-2/raw/snapshots/hide-undo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/hide-undo/stdout.log": { + "bytes": 442, + "sha256": "83829df81c7549cfe678508ad4c36fac33dfdc1b4dd3ca9e520528716f2bc151" + }, + "runs/run-2/raw/snapshots/move-redo/result.json": { + "bytes": 2755, + "sha256": "6af8e8bbe0e2fa11adc8dd11a02252c44d2931d36a95e4b7b5babda3c0b00c74" + }, + "runs/run-2/raw/snapshots/move-redo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/move-redo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/move-redo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/move-redo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/move-redo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/move-redo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/move-redo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/move-redo/source/views/diagram_oa_view.arcadia": { + "bytes": 1898, + "sha256": "204c6a537e51c8040787ad1ac5abb5f0b245c4e4b07575603363ad308d0889ca" + }, + "runs/run-2/raw/snapshots/move-redo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/move-redo/started.json": { + "bytes": 2353, + "sha256": "4afdfab983d97e44ae03d997d4ce8daeee4582a86d70d50694f3e186dd152a13" + }, + "runs/run-2/raw/snapshots/move-redo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/move-redo/stdout.log": { + "bytes": 469, + "sha256": "7a5d980929bd115c3c7accca4ee4865afe1de934074c6955293bf0828feb647a" + }, + "runs/run-2/raw/snapshots/move-undo/result.json": { + "bytes": 2759, + "sha256": "33966e16e17c85a8791ade230530e7797a2c959ac0d488d45dc8b9bf3828467f" + }, + "runs/run-2/raw/snapshots/move-undo/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/move-undo/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/move-undo/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/move-undo/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/move-undo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/move-undo/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/move-undo/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/move-undo/source/views/diagram_oa_view.arcadia": { + "bytes": 1896, + "sha256": "4e7d47d6a03153070687136c89faf01fc6df45ae89e2695beeab2fe4872dbc88" + }, + "runs/run-2/raw/snapshots/move-undo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/move-undo/started.json": { + "bytes": 2357, + "sha256": "34474429023d4bf8ddae53b2218af16f71b1325dd710b28bc0d6c7d9aa508458" + }, + "runs/run-2/raw/snapshots/move-undo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/move-undo/stdout.log": { + "bytes": 442, + "sha256": "0bca413db2f2f9aafb1731cb7d924ccb480f73efd9cfe73785085b774e1a3de6" + }, + "runs/run-2/raw/snapshots/move/result.json": { + "bytes": 2750, + "sha256": "8e75f198d56f9c3c73b7933b08b50aaeb74aac556f55894e3bdfcb3860c2e729" + }, + "runs/run-2/raw/snapshots/move/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/move/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/move/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/move/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/move/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/move/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/move/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/move/source/views/diagram_oa_view.arcadia": { + "bytes": 1898, + "sha256": "204c6a537e51c8040787ad1ac5abb5f0b245c4e4b07575603363ad308d0889ca" + }, + "runs/run-2/raw/snapshots/move/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/move/started.json": { + "bytes": 2348, + "sha256": "6cc14972b8d7b29d7253cccf50860c7f8ad55851b8a6dd9403cde69db82a5664" + }, + "runs/run-2/raw/snapshots/move/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/move/stdout.log": { + "bytes": 464, + "sha256": "e0047eef89262df29a870702f00b1219de98668e77a70717ab82424ee336256c" + }, + "runs/run-2/raw/snapshots/reopened-hidden/result.json": { + "bytes": 2760, + "sha256": "88912739288335d78a676cdfc2c688361aab5f3efb708a3b82b1c754741679c3" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/oa/activities/activity_activity_a.arcadia": { + "bytes": 474, + "sha256": "9bf7c5637d87c97213cf869118e942e8a29a67b429fa58e4de42235bba0980ee" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/oa/activities/activity_activity_b.arcadia": { + "bytes": 286, + "sha256": "152007669b7c570674aff99615a48a6f557be50f88bbbb3a5edc20b423c79a1f" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/oa/activities/activity_activity_c.arcadia": { + "bytes": 153, + "sha256": "d726b6b4e1200edd9ef752bb767e38d3d69dcac8eb6135d5f83c93832ff849b1" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/project.arcadia": { + "bytes": 597, + "sha256": "e7bd67c5785a78766298255ed3d53ff6d65dcd24bde1570ac8a0ff45cc9dc385" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/sa/functions/function_sa_function.arcadia": { + "bytes": 162, + "sha256": "1c724a9204784435e729badd4ad164b1a6b891840bb1ddc354ac1e175e3d712c" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/traceability.arcadia": { + "bytes": 522, + "sha256": "26b0b10715ced7aefcde941e6d1c1a6973f174997eae989e32a25c695dfb7cd1" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/views/diagram_oa_other.arcadia": { + "bytes": 315, + "sha256": "06ceef9cfdbb3ef229a22db6c48d8bb808388e027c37294648555ecbfb6bfab9" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/views/diagram_oa_view.arcadia": { + "bytes": 1652, + "sha256": "f2ee5975b2335415c1a6214e7eeccfc97715c53fdce617ddab595ba2acb087a1" + }, + "runs/run-2/raw/snapshots/reopened-hidden/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "c75b0fe14496631c9b017a32e277d1e5ad5b25e00e06b804fc54d950d7fb57f6" + }, + "runs/run-2/raw/snapshots/reopened-hidden/started.json": { + "bytes": 2358, + "sha256": "a5e053f7e932bc42a147b52aa8bc35e666b3e11c3f55f8a3b839827d7a24de41" + }, + "runs/run-2/raw/snapshots/reopened-hidden/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-2/raw/snapshots/reopened-hidden/stdout.log": { + "bytes": 443, + "sha256": "67ce5159055e3552b81af45e93f0203210d095159a7e18a7e62190f3ead3c3b8" + }, + "runs/run-2/raw/started.json": { + "bytes": 4015, + "sha256": "6e7afb815e35dd326e105f6777b99be49064ee20160017022833aa655223495d" + } + } +} diff --git a/harness/experiments/multiselect-20260923/native/prepare.py b/harness/experiments/multiselect-20260923/native/prepare.py new file mode 100644 index 00000000..d7d9dd62 --- /dev/null +++ b/harness/experiments/multiselect-20260923/native/prepare.py @@ -0,0 +1,129 @@ +"""Package explicit completed native runs; helpers are pinned data, never executed.""" +from pathlib import Path +import argparse +import json +import re +import zipfile +from verify import pin, parsed, need, norm, relative, safe, verify_data, verify_package + +HERE = Path(__file__).resolve().parent + + +def collect(root, runs): + native = root / ".local/multiselect-native" + candidates = [path for path in native.iterdir() if path.is_file() and path.suffix in (".py", ".js")] + candidates.extend(sorted((root / ".local/group-movement-gates").glob("*.py"))) + helpers = [(path.suffix, path.read_bytes()) for path in candidates] + payload, specs = {}, {} + def add(name, data): + need(safe(name) and (name not in payload or payload[name] == data), "member collision: " + name) + payload[name] = data + def tree(folder, prefix): + need(folder.is_dir(), "missing directory: " + str(folder)) + for path in sorted(folder.rglob("*")): + need(not path.is_symlink(), "symlink evidence input") + if path.is_file(): + add(prefix + path.relative_to(folder).as_posix(), path.read_bytes()) + for run in runs: + folder = native / run + start = parsed((folder / "started.json").read_bytes()) + end = parsed((folder / "finished.json").read_bytes()) + need(end["exitCode"] == 0 and all(end[k] is True for k in + ("guiUnchanged", "checkerUnchanged", "fixtureUnchanged")), "unfinished/changed native run: " + run) + generation = parsed((folder / "generation.json").read_bytes()) + history = parsed((folder / "history-bytes.json").read_bytes()) + tree(folder, "runs/" + run + "/raw/") + fixture_relative = relative(generation["command"][2], start["cwd"]) + need(fixture_relative.startswith(".local/multiselect-native/"), "fixture outside native root") + tree(root / fixture_relative, "runs/" + run + "/fixture/") + spec = {"originalRun": norm(start["cwd"]) + "/.local/multiselect-native/" + run, + "originalFixture": norm(generation["command"][2]), "helpers": {}, "builds": {}} + for key, expected in [("helper", start["helper"]), ("capture", start["capture"]), + ("snapshotHelper", start["snapshotHelper"]), ("history", history["helper"])]: + matches = [(suffix, data) for suffix, data in helpers if pin(data) == expected] + need(bool(matches), "exact historical helper unavailable: " + key) + suffix, data = matches[0] + member = "helpers/" + expected["sha256"] + suffix + add(member, data) + spec["helpers"][key] = member + for kind in ("gui", "checker"): + recorded = start[kind] + rel = relative(recorded["receipt"], start["cwd"]) + need(rel.startswith(".local/group-movement-gates/") and rel.endswith("/result.json"), "unexpected build receipt location") + receipt = root / rel + data = receipt.read_bytes() + need(pin(data) == recorded["receiptPin"], "historical build receipt changed") + prefix = "builds/" + pin(data)["sha256"] + "/" + add(prefix + "result.json", data) + inputs_data = (receipt.parent / "inputs.json").read_bytes() + need(pin(inputs_data) == recorded["inputs"], "historical build input map changed") + add(prefix + "inputs.json", inputs_data) + spec["builds"][kind] = {"receipt": prefix + "result.json", "inputs": prefix + "inputs.json", "originalRelative": rel} + build_helper = parsed(data)["helper"] + matching_helpers = [(suffix, value) for suffix, value in helpers if pin(value) == build_helper] + need(bool(matching_helpers), "exact historical build helper unavailable") + suffix, helper_data = matching_helpers[0] + helper_member = "helpers/" + build_helper["sha256"] + suffix + add(helper_member, helper_data) + spec["builds"][kind]["helper"] = helper_member + if kind == "checker": + # Read only the pinned historical example, not today's checkout. + archive_path = receipt.parent / "inputs.zip" + need(pin(archive_path.read_bytes()) == parsed(data)["archive"], "checker input archive pin") + source_path = "crates/oa-core/examples/multiselect_native.rs" + with zipfile.ZipFile(archive_path) as archive: + source = archive.read(source_path) + need(pin(source) == parsed(inputs_data)[source_path], "historical checker source pin") + member = "checker-source/" + pin(source)["sha256"] + ".rs" + add(member, source) + spec["checkerSource"] = {"inputPath": source_path, "member": member} + specs[run] = spec + manifest = {"schema": 1, "kind": "multiselect-native", "runs": specs, + "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 stream: + stream.write((json.dumps(value, indent=2) + "\n").encode()) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=HERE.parents[3]) + parser.add_argument("--runs", nargs="+", required=True) + parser.add_argument("--output", type=Path, default=HERE) + parser.add_argument("--check-only", action="store_true") + args = parser.parse_args() + need(len(args.runs) == len(set(args.runs)) and all(re.fullmatch(r"run-[0-9]+", name) for name in args.runs), "explicit unique run-N names required") + payload, manifest, summary = collect(args.root.resolve(), args.runs) + if args.check_only: + print("PASS raw consistency; no archive written:", summary) + return + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + for name in ("evidence.zip", "manifest.json", "files.json"): + need(not (out / name).exists(), "existing output refused: " + name) + for name in ("prepare.py", "verify.py", "README.md"): + data = (HERE / name).read_bytes() + if (out / name).exists(): + need((out / name).read_bytes() == data, "different existing package tool") + else: + with (out / name).open("xb") as stream: + stream.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 evidence") + print("PASS package readback:", out, summary) + + +if __name__ == "__main__": + main() diff --git a/harness/experiments/multiselect-20260923/native/verify.py b/harness/experiments/multiselect-20260923/native/verify.py new file mode 100644 index 00000000..b851a35e --- /dev/null +++ b/harness/experiments/multiselect-20260923/native/verify.py @@ -0,0 +1,235 @@ +"""Verify archived native receipts and bytes; do not rerun the GUI or checker.""" +from pathlib import Path, PurePosixPath +from datetime import datetime +import argparse +import hashlib +import json +import math +import stat +import zipfile + +BASE = {"started.json", "finished.json", "generation.json", "generation.stdout.log", + "generation.stderr.log", "app.stdout.log", "app.stderr.log", "processes.json", "history-bytes.json"} +GROUPS = [["baseline", "move-undo", "before-hide", "hide-undo"], ["move", "move-redo"], + ["hidden", "hide-redo", "reopened-hidden"]] +MODES = {name: mode for group, mode in zip(GROUPS, ["unchanged", "moved", "hide"]) for name in group} +OUTER = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"} + + +def need(ok, message): + if not ok: + raise ValueError(message) + + +def pin(data): + return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def safe(name): + return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name + and not PurePosixPath(name).is_absolute() and all(ord(c) >= 32 for c in name) + and all(part not in ("", ".", "..") for part in name.split("/"))) + + +def parsed(data): + def unique(items): + result = {} + for key, value in items: + need(key not in result, "duplicate JSON key: " + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=unique) + + +def norm(path): + return path.replace("\\", "/").rstrip("/") + + +def relative(path, root): + prefix = norm(root) + "/" + need(norm(path).startswith(prefix), "recorded path outside owner: " + path) + value = norm(path)[len(prefix):] + need(safe(value), "unsafe recorded relative path") + return value + + +def ordered(*values): + times = [datetime.fromisoformat(value) for value in values] + return all(t.utcoffset() is not None for t in times) and times == sorted(times) + + +def zip_inventory(archive, members): + names = archive.namelist() + need(len(names) == len(set(names)) and set(names) == set(members), "exact archive members") + for info in archive.infolist(): + need(safe(info.filename) and not info.is_dir() and not stat.S_ISLNK(info.external_attr >> 16), "unsafe archive member") + need(info.file_size == members[info.filename]["bytes"], "archive member size") + + +def verify_data(manifest, read): + need(manifest["schema"] == 1 and manifest["kind"] == "multiselect-native", "manifest kind") + members, runs = manifest["members"], manifest["runs"] + need(bool(runs) and all(safe(run) and "/" not in run for run in runs), "run names") + need(all(safe(name) for name in members), "safe member names") + for name, expected in members.items(): + need(pin(read(name)) == expected, "member pin: " + name) + used = set() + def raw(name): + need(name in members, "missing member: " + name) + used.add(name) + return read(name) + def obj(name): + return parsed(raw(name)) + def tree(prefix): + return {name[len(prefix):]: raw(name) for name in members if name.startswith(prefix)} + def pinned_tree(prefix, expected): + actual = tree(prefix) + need({name: pin(data) for name, data in actual.items()} == expected, "exact pinned tree: " + prefix) + return actual + for run, spec in runs.items(): + prefix = "runs/" + run + "/raw/" + start, end = obj(prefix + "started.json"), obj(prefix + "finished.json") + generation, process = obj(prefix + "generation.json"), obj(prefix + "processes.json") + history = obj(prefix + "history-bytes.json") + need(end["exitCode"] == 0 and all(end[key] is True for key in + ("guiUnchanged", "checkerUnchanged", "fixtureUnchanged")), run + ": clean unchanged closure") + need(ordered(generation["start"], generation["finish"], process["CreationDate"], start["start"], end["finish"], history["verified"]), run + ": process/generation times") + need(process["ProcessId"] == start["pid"] and type(start["pid"]) is int and start["pid"] > 0, run + ": process identity") + need(norm(process["ExecutablePath"]) == norm(start["gui"]["binary"]) + and start["command"] == [start["gui"]["binary"]], run + ": GUI command") + need(spec["originalRun"] == norm(start["cwd"]) + "/.local/multiselect-native/" + run, run + ": original run mapping") + need(generation["exitCode"] == 0 and generation["checker"] == start["checker"], run + ": generation checker") + need(generation["command"][:2] == [start["checker"]["binary"], "generate"] + and len(generation["command"]) == 3 and norm(generation["command"][2]) == spec["originalFixture"], run + ": generation command") + for stream in ("stdout", "stderr"): + need(end[stream] == pin(raw(prefix + "app." + stream + ".log")), run + ": app stream") + need(generation[stream] == pin(raw(prefix + "generation." + stream + ".log")), run + ": generation stream") + fixture = pinned_tree("runs/" + run + "/fixture/", start["fixture"]) + need(set(fixture) == {"baseline.json", "manifest.json"} | {"source/" + name for name in + obj(prefix + "snapshots/baseline/result.json")["source"]}, run + ": fixture membership") + fixture_manifest = parsed(fixture["manifest.json"]) + need(fixture_manifest["fixture"] == "multiselect-native-20260923" + and fixture_manifest["source_entry"] == "project.arcadia", run + ": fixture identity") + need(parsed(fixture["baseline.json"])["id"] == fixture_manifest["ids"]["project"], run + ": baseline identity") + for key, expected in [("helper", start["helper"]), ("capture", start["capture"]), + ("snapshotHelper", start["snapshotHelper"]), ("history", history["helper"])]: + need(pin(raw(spec["helpers"][key])) == expected, run + ": helper " + key) + builds = {} + for kind, target in [("gui", "arcadia-studio"), ("checker", "multiselect_native")]: + recorded = start[kind] + build = obj(spec["builds"][kind]["receipt"]) + need(pin(raw(spec["builds"][kind]["receipt"])) == recorded["receiptPin"], run + ": build receipt pin") + need(relative(recorded["receipt"], start["cwd"]) == spec["builds"][kind]["originalRelative"], run + ": build path mapping") + need(build["exitCode"] == 0 and build["inputsUnchanged"] is True + and ordered(build["start"], build["finish"], generation["start"]), run + ": accepted preceding build") + need(build["inputs"] == recorded["inputs"] == pin(raw(spec["builds"][kind]["inputs"])), run + ": build input map") + need(pin(raw(spec["builds"][kind]["helper"])) == build["helper"], run + ": build helper pin") + inputs = obj(spec["builds"][kind]["inputs"]) + need(len(inputs) == build["inputCount"] and sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": build input count") + artifacts = [item for item in build["artifacts"] if item["cargo"]["target"]["name"] == target] + need(len(artifacts) == 1 and artifacts[0]["readback"] == recorded["pin"] + and artifacts[0]["cargo"]["executable"] == recorded["binary"], run + ": executable relationship") + builds[kind] = inputs + need(start["gui"]["inputs"] == start["checker"]["inputs"], run + ": matching GUI/checker inputs") + source = spec["checkerSource"] + need(source["inputPath"] == "crates/oa-core/examples/multiselect_native.rs" + and pin(raw(source["member"])) == builds["checker"][source["inputPath"]], run + ": archived exact checker source") + + top = {name[len(prefix):] for name in members if name.startswith(prefix) and "/" not in name[len(prefix):]} + captures = sorted(name for name in top - BASE if name.endswith(".json")) + need(bool(captures), run + ": missing captures") + expected_top = set(BASE) | set(captures) + captured_window = None + for name in captures: + capture = obj(prefix + name) + need(ordered(start["start"], capture["time"], end["finish"]), run + ": capture lifetime") + window = capture["window"] + app = window["app"] + need(isinstance(app, str) and app.startswith("process:") + and norm(app[len("process:"):]) == norm(start["gui"]["binary"]), run + ": captured executable relationship") + need(type(window["id"]) is int and window["id"] > 0, run + ": captured window identity") + if captured_window is None: + captured_window = window["id"] + need(window["id"] == captured_window, run + ": inconsistent captured window identity") + need(isinstance(capture["images"], list), run + ": capture images") + for image in capture["images"]: + filename = image["file"] + need(safe(filename) and "/" not in filename and filename.startswith(name[:-5] + "-") + and filename.endswith((".jpg", ".png")), run + ": capture image mapping") + need(type(image["width"]) is int and image["width"] > 0 + and type(image["height"]) is int and image["height"] > 0, run + ": image dimensions") + need(filename not in expected_top, run + ": duplicate image reference") + expected_top.add(filename) + raw(prefix + filename) + need(top == expected_top, run + ": extra/missing top-level run files") + + snapshots = {name[len(prefix + "snapshots/"):].split("/")[0] for name in members if name.startswith(prefix + "snapshots/")} + need(snapshots == set(MODES), run + ": exact nine snapshot labels") + states = {} + for label, mode in MODES.items(): + folder = prefix + "snapshots/" + label + "/" + began, result = obj(folder + "started.json"), obj(folder + "result.json") + need(all(result.get(key) == value for key, value in began.items()), label + ": start/result") + need(result["exitCode"] == 0 and all(result[key] is True for key in + ("binaryUnchanged", "workingSourceUnchanged", "baselineUnchanged")), label + ": unchanged successful checker") + need(ordered(start["start"], result["start"], result["finish"], end["finish"]), label + ": checker lifetime") + need(result["buildReceipt"] == start["checker"]["receiptPin"] and result["binary"] == start["checker"]["pin"] + and result["helper"] == start["snapshotHelper"], label + ": checker provenance") + need(result["baseline"] == pin(fixture["baseline.json"]), label + ": baseline pin") + command = result["command"] + need(len(command) == 5 and command[:3] == [start["checker"]["binary"], "check", mode] + and norm(command[3]) == spec["originalFixture"] + "/baseline.json" + and norm(command[4]) == spec["originalRun"] + "/snapshots/" + label + "/source/project.arcadia", label + ": exact checker command") + for stream in ("stdout", "stderr"): + need(result[stream] == pin(raw(folder + stream + ".log")), label + ": stream pin") + need(not raw(folder + "stderr.log"), label + ": checker stderr") + report = obj(folder + "stdout.log") + need(report["result"] == "PASS" and report["comparison"] == "complete Project equality" + and report["mode"] == mode and report["baseline"] == command[3] and report["saved"] == command[4], label + ": positive checker report") + delta = report["inferred_common_delta"] + if mode == "moved": + need(isinstance(delta, dict) and set(delta) == {"x", "y"} + and all(type(v) in (int, float) and math.isfinite(v) and abs(v) <= 500 for v in delta.values()) + and math.hypot(delta["x"], delta["y"]) >= 10, label + ": inferred movement bounds") + else: + need(delta is None, label + ": unexpected inferred delta") + states[label] = pinned_tree(folder + "source/", result["source"]) + need(len(states[label]) == history["sourceFilesPerSnapshot"] == 9, label + ": source count") + expected = {folder + name for name in ("started.json", "result.json", "stdout.log", "stderr.log")} + expected.update(folder + "source/" + name for name in states[label]) + need({name for name in members if name.startswith(folder)} == expected, label + ": extra snapshot files") + need(history["status"] == "PASS" and history["equalByteGroups"] == GROUPS, run + ": history groups") + for group in GROUPS: + need(all(states[label] == states[group[0]] for label in group), run + ": actual history bytes " + str(group)) + need(all(states[a[0]] != states[b[0]] for i, a in enumerate(GROUPS) for b in GROUPS[i + 1:]), run + ": distinct history states") + need(states["baseline"] == {name[7:]: data for name, data in fixture.items() if name.startswith("source/")}, run + ": baseline source exact") + changed = {label: sorted(name for name in set(states["baseline"]) | set(states[label]) + if states["baseline"].get(name) != states[label].get(name)) for label in ("move", "hidden")} + need(changed == history["changedFilesFromBaseline"] + == {label: ["views/diagram_oa_view.arcadia"] for label in ("move", "hidden")}, run + ": changed source files") + print(run, "closed exit 0;", len(captures), "captures; nine positive snapshots; exact three-group source history") + need(used == set(members), "unreferenced/extra package members") + return {"runs": len(runs), "members": len(members)} + + +def verify_package(root): + files = parsed((root / "files.json").read_bytes()) + need(set(files) == OUTER, "outer files") + for name, expected in files.items(): + need(pin((root / name).read_bytes()) == expected, "outer pin: " + name) + manifest = parsed((root / "manifest.json").read_bytes()) + with zipfile.ZipFile(root / "evidence.zip") as archive: + zip_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 native evidence byte/receipt consistency:", verify_package(args.package)) + print("No GUI/checker execution; actions and native acceptance require the separate parent review.") + + +if __name__ == "__main__": + main() diff --git a/harness/experiments/multiselect-20260923/source/.gitattributes b/harness/experiments/multiselect-20260923/source/.gitattributes new file mode 100644 index 00000000..5b2d89c3 --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/.gitattributes @@ -0,0 +1,2 @@ +# Preserve pinned historical receipts and helper bytes through Git round trips. +* -text whitespace=cr-at-eol diff --git a/harness/experiments/multiselect-20260923/source/README.md b/harness/experiments/multiselect-20260923/source/README.md new file mode 100644 index 00000000..fcbeead5 --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/README.md @@ -0,0 +1,52 @@ +# Captured inputs and committed source identity + +This directory preserves exact historical `committed-desktop-4.json` and +`committed-desktop-5.json` receipts plus `verify-committed-inputs.py` as data. +The original helper contains its historical absolute checkout path; do not run +it for portable verification. The receipts do not themselves pin that helper; +`files.json` pins the copies distributed here and the new verifier/README. +The local `.gitattributes` preserves those pinned bytes through Git round trips; +retain it with this directory rather than normalizing the historical text. + +With Python 3.10+, Git, a repository containing both commits, and the completed +sibling checks package, run: + +```text +python -B SOURCE_DIRECTORY/verify.py --repo REPOSITORY --checks CHECKS_PACKAGE_DIRECTORY +``` + +Before that package exists, an explicitly optional raw-input mode is available: + +```text +python -B harness/experiments/multiselect-20260923/source/verify.py --repo . --raw-gates .local/group-movement-gates +``` + +The verifier checks source-evidence pins, captured map byte hashes, recorded gate +success/unchanged flags, counts and 71 vendored iced_widget inputs. It compares the +complete relevant Git tree inventory and every blob's size/SHA-256 at these +commits with their corresponding captured input maps: + +- `desktop-4`: `12a7352ddef5f69a144ead8219a50916233d63a9` +- `desktop-5`: `f6a6010baed9e6164a1fcc7ecd58256a562827d7` + +The scope matches the historical helper: Cargo manifests/lock/toolchain file, +`crates/`, `build/`, `.cargo/`, `scripts/`, `docs/`, `examples/`, +`harness/fixtures/`, and `harness/prototypes/`. This is the complete inventory +within that scope, not a whole-repository identity claim. Worktree contents, +current HEAD, line-ending checkout conversion and original machine paths do not +participate. Git objects are read by object ID; no checkout, build or archived +helper runs. Both commits must already be available; lazy object fetching is +disabled for these reads. + +In package mode, outer checks-package pins and selected member/run relationships +are also checked. Run the checks package's own verifier for its full source ZIP, +logs, helpers and all other gate relationships. Raw mode reads the named local +maps/receipts directly and is not a substitute for packaged-evidence verification. +No source maps or ZIPs are duplicated in this directory. + +Historical receipts report an earlier comparison. A successful run of this new +verifier performs a fresh Git-object comparison against the pinned maps. A gate's +recorded HEAD can predate its later source-identical commit; that difference is +why blob comparison is necessary. This evidence identifies tested source bytes; +it does not rerun tests or establish native usability, release integration or +full #29 acceptance. diff --git a/harness/experiments/multiselect-20260923/source/committed-desktop-4.json b/harness/experiments/multiselect-20260923/source/committed-desktop-4.json new file mode 100644 index 00000000..291ee181 --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/committed-desktop-4.json @@ -0,0 +1,11 @@ +{ + "checked": "2026-09-23T08:11:54.115058+00:00", + "commit": "12a7352ddef5f69a144ead8219a50916233d63a9", + "gate": "desktop-4", + "inputCount": 729, + "inputMap": { + "bytes": 121985, + "sha256": "55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb" + }, + "comparison": "Exact committed Git blobs equal every captured input; same complete filtered inventory" +} \ No newline at end of file diff --git a/harness/experiments/multiselect-20260923/source/committed-desktop-5.json b/harness/experiments/multiselect-20260923/source/committed-desktop-5.json new file mode 100644 index 00000000..2511f88f --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/committed-desktop-5.json @@ -0,0 +1,11 @@ +{ + "checked": "2026-09-23T08:53:15.452246+00:00", + "commit": "f6a6010baed9e6164a1fcc7ecd58256a562827d7", + "gate": "desktop-5", + "inputCount": 729, + "inputMap": { + "bytes": 121985, + "sha256": "99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c" + }, + "comparison": "Exact committed Git blobs equal every captured input; same complete filtered inventory" +} \ No newline at end of file diff --git a/harness/experiments/multiselect-20260923/source/files.json b/harness/experiments/multiselect-20260923/source/files.json new file mode 100644 index 00000000..bf5d230d --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/files.json @@ -0,0 +1,26 @@ +{ + ".gitattributes": { + "bytes": 109, + "sha256": "aca8eef17dbab8cfedcbe18e1175cfee115802376fac4ec69e8c3320a6399d6c" + }, + "README.md": { + "bytes": 2792, + "sha256": "b8db0c2c71e491b6179b3e57d27e5dfb23c9283894219feea328bc632603f2aa" + }, + "verify.py": { + "bytes": 7255, + "sha256": "2ed667c127470efb669fb43d5dcdb288f891e42e10e67ef98ca4cd1125df09d1" + }, + "verify-committed-inputs.py": { + "bytes": 2138, + "sha256": "ac2b50a73f678456b9628684347484c013f5f9c78f054de72b74f56a34c990d5" + }, + "committed-desktop-4.json": { + "bytes": 390, + "sha256": "47c6774bcb7d0c37c50e0ed50f8fb58ee9b55c6fcee30af488481c314a4e66e8" + }, + "committed-desktop-5.json": { + "bytes": 390, + "sha256": "da1f08e06bd0af6512da4196618b71d0c4ecc090012f061bb82b6a36f5b7686c" + } +} diff --git a/harness/experiments/multiselect-20260923/source/verify-committed-inputs.py b/harness/experiments/multiselect-20260923/source/verify-committed-inputs.py new file mode 100644 index 00000000..ef03a671 --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/verify-committed-inputs.py @@ -0,0 +1,36 @@ +"""Check the tested source inventory against exact Git blobs, not HEAD alone.""" +from pathlib import Path +import datetime, hashlib, json, subprocess, sys +root=Path('C:/ar-multiselect-9e8r6t') +run,=sys.argv[1:] +gate=root/'.local/group-movement-gates'/run +inputs=json.loads((gate/'inputs.json').read_bytes()) +result=json.loads((gate/'result.json').read_bytes()) +assert result['exitCode']==0 and result['inputsUnchanged'] +head=subprocess.check_output(['git','rev-parse','HEAD'],cwd=root,text=True).strip() +def relevant(p): + return p in ('Cargo.toml','Cargo.lock','rust-toolchain.toml') or p.startswith(('crates/','build/','.cargo/','scripts/','docs/','examples/','harness/fixtures/','harness/prototypes/')) +paths=subprocess.check_output(['git','ls-tree','-r','--name-only','-z',head],cwd=root).decode().split('\0') +assert set(inputs)=={p for p in paths if p and relevant(p)}, 'Committed input inventory differs' +assert all('\n' not in p for p in inputs) +request=''.join(head+':'+p+'\n' for p in inputs).encode() +raw=subprocess.run(['git','cat-file','--batch'],cwd=root,input=request,capture_output=True,check=True).stdout +offset=0 +actual={} +for path in inputs: + end=raw.index(b'\n',offset) + oid,kind,size=raw[offset:end].split() + assert kind==b'blob' + size=int(size) + data=raw[end+1:end+1+size] + assert len(data)==size and raw[end+1+size:end+2+size]==b'\n' + actual[path]={'bytes':size,'sha256':hashlib.sha256(data).hexdigest()} + offset=end+2+size +assert offset==len(raw) +changed=[p for p in inputs if inputs[p]!=actual[p]] +assert not changed, 'Tested bytes differ from committed blobs: '+repr(changed) +data=(gate/'inputs.json').read_bytes() +report={'checked':datetime.datetime.now(datetime.timezone.utc).isoformat(),'commit':head,'gate':run,'inputCount':len(inputs),'inputMap':{'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()},'comparison':'Exact committed Git blobs equal every captured input; same complete filtered inventory'} +out=root/'.local/group-movement-gates'/('committed-'+run+'.json') +with out.open('x',encoding='utf-8') as f: json.dump(report,f,indent=2) +print(json.dumps(report,indent=2)) diff --git a/harness/experiments/multiselect-20260923/source/verify.py b/harness/experiments/multiselect-20260923/source/verify.py new file mode 100644 index 00000000..7cfe5e47 --- /dev/null +++ b/harness/experiments/multiselect-20260923/source/verify.py @@ -0,0 +1,149 @@ +"""Compare captured input maps to exact named Git trees; no builds or checkouts.""" +from pathlib import Path, PurePosixPath +import argparse +import hashlib +import json +import os +import subprocess +import zipfile + +HERE = Path(__file__).resolve().parent +COMMITS = {"desktop-4": "12a7352ddef5f69a144ead8219a50916233d63a9", + "desktop-5": "f6a6010baed9e6164a1fcc7ecd58256a562827d7"} +COMPARISON = "Exact committed Git blobs equal every captured input; same complete filtered inventory" +FILES = {".gitattributes", "verify.py", "README.md", "verify-committed-inputs.py", + "committed-desktop-4.json", "committed-desktop-5.json"} + + +def need(ok, message): + if not ok: + raise ValueError(message) + + +def pin(data): + return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def parsed(data): + def unique(items): + result = {} + for key, value in items: + need(key not in result, "duplicate JSON key: " + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=unique) + + +def safe(name): + return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name + and not PurePosixPath(name).is_absolute() + and all(part not in ("", ".", "..") for part in name.split("/"))) + + +def relevant(path): + return path in ("Cargo.toml", "Cargo.lock", "rust-toolchain.toml") or path.startswith( + ("crates/", "build/", ".cargo/", "scripts/", "docs/", "examples/", "harness/fixtures/", "harness/prototypes/")) + + +def git(repo, *arguments, data=None): + environment = dict(os.environ, GIT_NO_LAZY_FETCH="1") + return subprocess.run(["git", *arguments], cwd=repo, input=data, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True, env=environment).stdout + + +def compare_commit(repo, commit, inputs): + need(git(repo, "rev-parse", "--verify", commit + "^{commit}").decode().strip() == commit, "named commit unavailable") + entries = git(repo, "ls-tree", "-r", "--full-tree", "-z", commit).split(b"\0") + blobs = {} + for entry in filter(None, entries): + header, path_bytes = entry.split(b"\t", 1) + path = path_bytes.decode("utf-8") + if relevant(path): + _mode, kind, oid = header.split() + need(kind == b"blob", "relevant non-blob tree entry: " + path) + need(path not in blobs, "duplicate tree path") + blobs[path] = oid + need(set(blobs) == set(inputs), "complete filtered Git inventory differs; missing=" + + repr(sorted(set(inputs) - set(blobs))) + "; additional=" + repr(sorted(set(blobs) - set(inputs)))) + # Object IDs avoid path quoting and commit:path interpretation in batch input. + paths = sorted(blobs) + data = git(repo, "cat-file", "--batch", data=b"".join(blobs[path] + b"\n" for path in paths)) + offset, actual = 0, {} + for path in paths: + end = data.index(b"\n", offset) + oid, kind, size_bytes = data[offset:end].split() + size = int(size_bytes) + need(oid == blobs[path] and kind == b"blob" and size >= 0, "Git batch object relationship") + content = data[end + 1:end + 1 + size] + need(len(content) == size and data[end + 1 + size:end + 2 + size] == b"\n", "Git batch framing") + actual[path] = pin(content) + offset = end + 2 + size + need(offset == len(data), "trailing Git batch bytes") + different = [path for path in paths if actual[path] != inputs[path]] + need(not different, "captured bytes differ from named commit: " + repr(different)) + + +def packaged(checks, runs): + files = parsed((checks / "files.json").read_bytes()) + need(set(files) == {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}, "checks outer inventory") + for name, expected in files.items(): + need(pin((checks / name).read_bytes()) == expected, "checks outer pin: " + name) + manifest = parsed((checks / "manifest.json").read_bytes()) + need(manifest["schema"] == 1 and manifest["kind"] == "multiselect-cargo-gates", "checks manifest kind") + captured = {} + with zipfile.ZipFile(checks / "evidence.zip") as archive: + names = archive.namelist() + need(len(names) == len(set(names)) and set(names) == set(manifest["members"]) + and all(safe(name) for name in names), "checks exact safe members") + def raw(logical): + member = manifest["logicalFiles"][logical] + data = archive.read(member) + need(pin(data) == manifest["members"][member], "checks member pin: " + logical) + return data + for run in runs: + prefix = "cargo/" + run + "/" + result = parsed(raw(prefix + "result.json")) + spec = manifest["runs"][run] + need(all(result[key] == spec[key] for key in ("command", "head", "exitCode", "inputCount")), run + ": checks run relationships") + captured[run] = (raw(prefix + "inputs.json"), result) + return captured + + +def verify(repo, captured): + outer = parsed((HERE / "files.json").read_bytes()) + need(set(outer) == FILES, "source evidence file inventory") + for name, expected in outer.items(): + need(pin((HERE / name).read_bytes()) == expected, "source evidence pin: " + name) + for run, commit in COMMITS.items(): + receipt = parsed((HERE / ("committed-" + run + ".json")).read_bytes()) + need(receipt["gate"] == run and receipt["commit"] == commit + and receipt["comparison"] == COMPARISON, run + ": historical identity relationship") + map_bytes, result = captured[run] + inputs = parsed(map_bytes) + need(pin(map_bytes) == receipt["inputMap"] == result["inputs"], run + ": exact captured input map") + need(result["exitCode"] == 0 and result["inputsUnchanged"] is True, run + ": accepted unchanged gate") + need(len(inputs) == receipt["inputCount"] == result["inputCount"], run + ": input counts") + need(all(safe(path) and relevant(path) for path in inputs), run + ": input scope") + need(sum(path.startswith("crates/vendor/iced_widget/") for path in inputs) == 71, run + ": vendor count") + compare_commit(repo, commit, inputs) + print("PASS exact Git tree/blobs:", run, commit, len(inputs), "captured inputs") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", type=Path, required=True, help="repository with both named commits already present") + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--checks", type=Path, help="portable checks package directory") + source.add_argument("--raw-gates", type=Path, help="optional preparation mode: raw gate directory, not a packaged-evidence verification") + args = parser.parse_args() + if args.checks: + captured = packaged(args.checks, COMMITS) + else: + captured = {run: ((args.raw_gates / run / "inputs.json").read_bytes(), + parsed((args.raw_gates / run / "result.json").read_bytes())) for run in COMMITS} + verify(args.repo, captured) + print("Source identity only; no build, checkout, helper execution or native acceptance claim.") + + +if __name__ == "__main__": + main() diff --git a/harness/experiments/process-column-slots-20260923/README.md b/harness/experiments/process-column-slots-20260923/README.md new file mode 100644 index 00000000..61f2f93d --- /dev/null +++ b/harness/experiments/process-column-slots-20260923/README.md @@ -0,0 +1,23 @@ +# Process column-slot experiment — exploratory evidence + +Question: does simultaneous column-slot permutation preserve presentation boundaries and stable order without hidden no-op mutations? Separately, can object-anchored Before/After retain selected-target no-op while reaching every useful destination? + +Expected: preserve the entire ID set, exact other-column indices, selected source order, unselected within-column order, a contiguous moved block, equivalent normalized-gap results, click-order independence and frozen ID-to-rank map. Identical column order must return unchanged. Cross-column selections must refuse. + +Run from the repository root with `node harness/experiments/process-column-slots-20260923/experiment.cjs` and `node harness/experiments/process-column-slots-20260923/anchor-policies.cjs`. Scripts overwrite only their own JSON results. Node was the existing bundled runtime. The parent inspected and reran both original scripts before copying them here. No Cargo, production edits or GUI were used by the experiment. + +Results: both scripts exited 0. The first exhausted every steps permutation, set partition, nonempty selected subset and column gap through five IDs: 214,990 admitted property cases, 140,024 cross-column refusals and 127,950 unchanged cases. It separately exhausted all 4,096 directed simple graphs on four IDs and their 24 step permutations: 98,304 graph orders, 861,096 admitted plans, zero changed-rank counterexamples. Its longest-path/fallback rank model follows the documented algorithm but does not execute Rust. The no-source-change assertion uses a sentinel JSON envelope and suppresses serialization for unchanged results; it does not prove the application source writer or history implementation. + +Concrete counterexample to global splicing: `[a,u,b,v,c]`, column `[a,b,c]`, selection `{b,c}`, after `a` is unchanged. Column-slot permutation preserves the exact original array; global splicing would write `[a,b,c,u,v]`. Moving that block before `a` produces `[b,u,c,v,a]`: `u` and `v` retain exact indices. Consequently, preserving all globally unselected relative order is intentionally not a requirement (`a` moves relative to `u` and `v`). + +Anchor-policy follow-up: all nonempty subsets for column lengths 1..10 (2,036 cases, 18,324 unselected Before/After anchors) reached every remainder-list gap using an unselected neighbor. Raw normalization differs from selected-anchor no-op in 17,004 selected-anchor cases. Example: `[a,b,c,d]`, selection `{b,d}`, Before `d` normalizes to `[a,c,b,d]`; the object-anchor policy keeps `[a,b,c,d]`. The changed result is still reachable through After `c`. The APIs are distinct: normalization does not automatically supersede selected-target no-op. Prefer retaining the latter for object-anchored UI if that is the chosen interaction; every changed result remains reachable. When all are selected, the sole destination is inherently unchanged. + +The selected-anchor no-op guard belongs before invoking a block planner: noncontiguous selected items can legitimately remain noncontiguous after that no-op. The block-contiguity postcondition applies to planned moves, not every selected-target gesture. + +Legacy exchange-only ordering assigns successive ranks, and cycle-blocked fallback assigns unique ranks. Multi-member sets spanning these ranks refuse. Four-node enumeration cannot exercise a changed multiple-member group beside a cycle: two cycle nodes leave only two peers, whose entire block has nowhere to move. Independent review identified this limit; an added five-node case checks `[a,u,b,v,c]`, cycle `u→v→u`, selection `{b,c}` before `a`, exact result `[b,u,c,v,a]`, and unchanged full ranks. Production should still validate the complete recomputed rank map. The experiment freezes graph edges; it does not rederive legacy OrderedPath adjacency. Valid legacy columns contain one node, so admitted moves are no-op and multiple-member groups refuse under this candidate. + +The same independent reviewer confirmed the rank model agrees with current Rust, found no planner counterexample under its assumptions, and noted that anchor reachability does not execute a selected-target guard or test actual pointer hit regions. Existing singleton reorder globally splices steps, so routing it through this planner would intentionally change invisible cross-column source ordering; the candidate must make that migration explicit. + +Limitations and next Rust checks: these scripts assume valid unique step IDs, valid complete rank maps, visible local occurrence selection, and valid destination bounds. Rust must reject duplicate/missing/wrong-owner IDs, qualified/link mixtures, stale snapshots and dirty drafts before side effects. Explicitly choose raw-gap versus object-anchor input; keep before-first/after-last reachable. Verify production layout with different card heights and expansion, preview/commit plan identity, OA/SA source roundtrip, unchanged semantics outside steps, one Undo/Redo, no history for no-op, source limits/refusal, and native slot feedback. Deletion mode switching and aggregate removal were not investigated by these movement scripts. + +Artifacts are exploratory evidence. They do not establish State acceptance or Process implementation acceptance. diff --git a/harness/experiments/process-column-slots-20260923/anchor-policies.cjs b/harness/experiments/process-column-slots-20260923/anchor-policies.cjs new file mode 100644 index 00000000..4158b0cf --- /dev/null +++ b/harness/experiments/process-column-slots-20260923/anchor-policies.cjs @@ -0,0 +1,31 @@ +const fs=require('fs'), assert=require('assert/strict'); +let cases=0, anchors=0, divergent=0; +for(let n=1;n<=10;n++) { + const column=Array.from({length:n},(_,i)=>i); + for(let mask=1;mask<(1<mask&(1<!selected.has(id)); + const reached=new Set(); + for(const anchor of remainder) for(const after of [false,true]) { + const rawGap=column.indexOf(anchor)+(after?1:0); + const gap=column.slice(0,rawGap).filter(id=>!selected.has(id)).length; + reached.add(gap); anchors++; + assert.equal(gap,remainder.indexOf(anchor)+(after?1:0)); + } + if(remainder.length) assert.deepEqual([...reached].sort((a,b)=>a-b),Array.from({length:remainder.length+1},(_,i)=>i)); + else assert.equal(selected.size,n); // Only one mathematical gap, all-selected no-op. + for(const anchor of selected) for(const after of [false,true]) { + const rawGap=column.indexOf(anchor)+(after?1:0); + const gap=column.slice(0,rawGap).filter(id=>!selected.has(id)).length; + const block=column.filter(id=>selected.has(id)); + const normalized=[...remainder.slice(0,gap),...block,...remainder.slice(gap)]; + if(normalized.some((id,i)=>id!==column[i]))divergent++; + // Object policy intentionally preserves original order for selected anchors. + // It need not form a contiguous block when this operation is a no-op. + } + cases++; + } +} +const result={passed:true,method:'All nonempty subsets for column lengths 1..10, all unselected and selected Before/After anchors. Order-label equivariance makes separate ID permutations redundant for this reachability property.',cases,unselectedAnchors:anchors,selectedAnchorRawNormalizationDivergences:divergent,conclusion:'Every remainder gap is reachable via an unselected Before/After anchor. Selected-anchor no-op does not remove any possible changed result. Raw-gap and object-anchor policies remain distinct APIs; neither silently supersedes the other.',counterexample:{column:['a','b','c','d'],selected:['b','d'],drop:'Before d',rawGapResult:['a','c','b','d'],objectAnchorResult:['a','b','c','d'],equivalentUnselectedDrop:'After c'}}; +fs.writeFileSync(__dirname+'/anchor-results.json',JSON.stringify(result,null,2)+'\n'); +console.log(JSON.stringify(result,null,2)); diff --git a/harness/experiments/process-column-slots-20260923/anchor-results.json b/harness/experiments/process-column-slots-20260923/anchor-results.json new file mode 100644 index 00000000..4a3739ad --- /dev/null +++ b/harness/experiments/process-column-slots-20260923/anchor-results.json @@ -0,0 +1,34 @@ +{ + "passed": true, + "method": "All nonempty subsets for column lengths 1..10, all unselected and selected Before/After anchors. Order-label equivariance makes separate ID permutations redundant for this reachability property.", + "cases": 2036, + "unselectedAnchors": 18324, + "selectedAnchorRawNormalizationDivergences": 17004, + "conclusion": "Every remainder gap is reachable via an unselected Before/After anchor. Selected-anchor no-op does not remove any possible changed result. Raw-gap and object-anchor policies remain distinct APIs; neither silently supersedes the other.", + "counterexample": { + "column": [ + "a", + "b", + "c", + "d" + ], + "selected": [ + "b", + "d" + ], + "drop": "Before d", + "rawGapResult": [ + "a", + "c", + "b", + "d" + ], + "objectAnchorResult": [ + "a", + "b", + "c", + "d" + ], + "equivalentUnselectedDrop": "After c" + } +} diff --git a/harness/experiments/process-column-slots-20260923/experiment.cjs b/harness/experiments/process-column-slots-20260923/experiment.cjs new file mode 100644 index 00000000..74414312 --- /dev/null +++ b/harness/experiments/process-column-slots-20260923/experiment.cjs @@ -0,0 +1,119 @@ +// Exploratory model, not production code or a test of the Rust implementation. +const fs = require('fs'); +const assert = require('assert/strict'); +let count = 0, graphCount = 0, admitted = 0, refused = 0, noops = 0; +function* permutations(a) { + if (!a.length) { yield []; return; } + for (let i=0;ii!==j))) yield [a[i],...tail]; +} +function* partitions(n, prefix=[]) { + if(prefix.length===n) { yield prefix; return; } + const max=prefix.length?Math.max(...prefix)+1:0; + for(let x=0;x<=max;x++) yield* partitions(n,[...prefix,x]); +} +function plan(steps, ranks, selected, rawGap) { + const rank=ranks[selected[0]]; + if(!selected.length || selected.some(id=>ranks[id]!==rank)) return null; + const indices=steps.flatMap((id,i)=>ranks[id]===rank?[i]:[]); + const column=indices.map(i=>steps[i]), set=new Set(selected); + assert(rawGap>=0 && rawGap<=column.length); + const block=column.filter(id=>set.has(id)); + const remainder=column.filter(id=>!set.has(id)); + const gap=column.slice(0,rawGap).filter(id=>!set.has(id)).length; + const order=[...remainder.slice(0,gap),...block,...remainder.slice(gap)]; + const result=steps.slice(); + indices.forEach((index,i)=>result[index]=order[i]); + return {result, order, gap, changed:result.some((id,i)=>id!==steps[i])}; +} +function verify(steps,ranks,selected,rawGap,p) { + const set=new Set(selected), rank=ranks[selected[0]]; + const old=steps.filter(id=>ranks[id]===rank), got=p.result.filter(id=>ranks[id]===rank); + assert.deepEqual([...p.result].sort(),[...steps].sort()); + steps.forEach((id,i)=>{if(ranks[id]!==rank) assert.equal(p.result[i],id);}); + assert.deepEqual(got.filter(id=>set.has(id)),old.filter(id=>set.has(id))); + assert.deepEqual(got.filter(id=>!set.has(id)),old.filter(id=>!set.has(id))); + const first=got.findIndex(id=>set.has(id)); + assert.equal(got.slice(first,first+set.size).every(id=>set.has(id)),true); + // Independent gap oracle: every original unselected item before boundary + // precedes every selected item; all remaining unselected items follow them. + old.forEach((id,i)=>{if(!set.has(id)) selected.forEach(s=>assert.equal(got.indexOf(id)id===got[i]); + assert.equal(p.changed,!stableAlready); + const source=JSON.stringify({steps,untouched:'sentinel'}); + const after=p.changed?JSON.stringify({steps:p.result,untouched:'sentinel'}):source; + if(stableAlready) {assert.equal(after,source); noops++;} + count++; +} +for(let n=1;n<=5;n++) { + const ids=Array.from({length:n},(_,i)=>i); + for(const ranks of partitions(n)) for(const steps of permutations(ids)) { + for(let mask=1;mask<(1<mask&(1<ranks[id]!==ranks[selected[0]])) { + assert.equal(plan(steps,ranks,selected,0),null); refused++; continue; + } + const column=steps.filter(id=>ranks[id]===ranks[selected[0]]); + const equivalents=new Map(); + for(let gap=0;gap<=column.length;gap++) { + const p=plan(steps,ranks,selected,gap); + verify(steps,ranks,selected,gap,p); + assert.deepEqual(p,plan(steps,ranks,[...selected].reverse(),gap)); + if(equivalents.has(p.gap)) assert.deepEqual(p.result,equivalents.get(p.gap)); + equivalents.set(p.gap,p.result); + } + } + } +} +// Semantic column model: longest-path Kahn ranks and deterministic residual +// fallback, matching the documented algorithm, independently of planner. +function ranksOf(steps,edges) { + const indegree=Object.fromEntries(steps.map(id=>[id,0])); + const ranks=Object.fromEntries(steps.map(id=>[id,0])); + edges.forEach(([,b])=>indegree[b]++); + const queue=steps.filter(id=>indegree[id]===0), done=new Set(); + for(let i=0;i{if(!done.has(id))ranks[id]=fallback++;}); + return ranks; +} +const ids=[0,1,2,3], possible=ids.flatMap(a=>ids.filter(b=>a!==b).map(b=>[a,b])); +for(let mask=0;mask<(1<mask&(1<subset&(1<ranks[id]!==ranks[selected[0]]))continue; + const column=steps.filter(id=>ranks[id]===ranks[selected[0]]); + for(let gap=0;gap<=column.length;gap++) { + const p=plan(steps,ranks,selected,gap); + assert.deepEqual(ranksOf(p.result,edges),ranks); admitted++; + } + } + } +} +const concrete=[]; +function example(name,steps,ranks,selected,gap,expected) { + const p=plan(steps,ranks,selected,gap); assert.deepEqual(p?.result??null,expected); + concrete.push({name,steps,ranks,selected,rawGap:gap,result:p?.result??null,changed:p?.changed??false}); +} +example('interleaved visual no-op',['a','u','b','v','c'],{a:0,b:0,c:0,u:1,v:1},['c','b'],1,['a','u','b','v','c']); +example('column slot move preserves other indices',['a','u','b','v','c'],{a:0,b:0,c:0,u:1,v:1},['c','b'],0,['b','u','c','v','a']); +example('selected-anchor normalization is not always no-op',['a','b','c','d'],{a:0,b:0,c:0,d:0},['b','d'],3,['a','c','b','d']); +example('legacy successive-rank refusal',['a','b','c'],{a:0,b:1,c:2},['a','c'],0,null); +const cyclicRanks=ranksOf([0,1,2,3],[[2,3],[3,2]]); +example('cycle fallback group refusal',[0,1,2,3],cyclicRanks,[2,3],0,null); +example('acyclic peers beside cycle',[0,1,2,3],cyclicRanks,[1],0,[1,0,2,3]); +const besideCycleSteps=['a','u','b','v','c'], besideCycleEdges=[['u','v'],['v','u']]; +const besideCycleRanks=ranksOf(besideCycleSteps,besideCycleEdges); +example('changed multiple-member group beside cycle',besideCycleSteps,besideCycleRanks,['c','b'],0,['b','u','c','v','a']); +assert.deepEqual(ranksOf(['b','u','c','v','a'],besideCycleEdges),besideCycleRanks); +const result={expectation:'Column-slot permutation satisfies stable-block/gap/index invariants; recomputed ranks remain exact.',method:'All permutations and set partitions through n=5; every nonempty subset and raw column gap. Every directed simple graph on 4 IDs, all 24 step permutations, all admitted subsets/gaps. No Rust, source serializer, or GUI exercised.',passed:true,counts:{propertyCases:count,crossColumnRefusals:refused,noops,graphOrders:graphCount,rankCheckedPlans:admitted},concrete}; +fs.writeFileSync(__dirname+'/results.json',JSON.stringify(result,null,2)+'\n'); +console.log(JSON.stringify(result,null,2)); diff --git a/harness/experiments/process-column-slots-20260923/results.json b/harness/experiments/process-column-slots-20260923/results.json new file mode 100644 index 00000000..373edb23 --- /dev/null +++ b/harness/experiments/process-column-slots-20260923/results.json @@ -0,0 +1,199 @@ +{ + "expectation": "Column-slot permutation satisfies stable-block/gap/index invariants; recomputed ranks remain exact.", + "method": "All permutations and set partitions through n=5; every nonempty subset and raw column gap. Every directed simple graph on 4 IDs, all 24 step permutations, all admitted subsets/gaps. No Rust, source serializer, or GUI exercised.", + "passed": true, + "counts": { + "propertyCases": 214990, + "crossColumnRefusals": 140024, + "noops": 127950, + "graphOrders": 98304, + "rankCheckedPlans": 861096 + }, + "concrete": [ + { + "name": "interleaved visual no-op", + "steps": [ + "a", + "u", + "b", + "v", + "c" + ], + "ranks": { + "a": 0, + "b": 0, + "c": 0, + "u": 1, + "v": 1 + }, + "selected": [ + "c", + "b" + ], + "rawGap": 1, + "result": [ + "a", + "u", + "b", + "v", + "c" + ], + "changed": false + }, + { + "name": "column slot move preserves other indices", + "steps": [ + "a", + "u", + "b", + "v", + "c" + ], + "ranks": { + "a": 0, + "b": 0, + "c": 0, + "u": 1, + "v": 1 + }, + "selected": [ + "c", + "b" + ], + "rawGap": 0, + "result": [ + "b", + "u", + "c", + "v", + "a" + ], + "changed": true + }, + { + "name": "selected-anchor normalization is not always no-op", + "steps": [ + "a", + "b", + "c", + "d" + ], + "ranks": { + "a": 0, + "b": 0, + "c": 0, + "d": 0 + }, + "selected": [ + "b", + "d" + ], + "rawGap": 3, + "result": [ + "a", + "c", + "b", + "d" + ], + "changed": true + }, + { + "name": "legacy successive-rank refusal", + "steps": [ + "a", + "b", + "c" + ], + "ranks": { + "a": 0, + "b": 1, + "c": 2 + }, + "selected": [ + "a", + "c" + ], + "rawGap": 0, + "result": null, + "changed": false + }, + { + "name": "cycle fallback group refusal", + "steps": [ + 0, + 1, + 2, + 3 + ], + "ranks": { + "0": 0, + "1": 0, + "2": 1, + "3": 2 + }, + "selected": [ + 2, + 3 + ], + "rawGap": 0, + "result": null, + "changed": false + }, + { + "name": "acyclic peers beside cycle", + "steps": [ + 0, + 1, + 2, + 3 + ], + "ranks": { + "0": 0, + "1": 0, + "2": 1, + "3": 2 + }, + "selected": [ + 1 + ], + "rawGap": 0, + "result": [ + 1, + 0, + 2, + 3 + ], + "changed": true + }, + { + "name": "changed multiple-member group beside cycle", + "steps": [ + "a", + "u", + "b", + "v", + "c" + ], + "ranks": { + "a": 0, + "u": 1, + "b": 0, + "v": 2, + "c": 0 + }, + "selected": [ + "c", + "b" + ], + "rawGap": 0, + "result": [ + "b", + "u", + "c", + "v", + "a" + ], + "changed": true + } + ] +} diff --git a/harness/experiments/state-multiselect-20260923/.gitattributes b/harness/experiments/state-multiselect-20260923/.gitattributes new file mode 100644 index 00000000..082badab --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/.gitattributes @@ -0,0 +1,3 @@ +# Preserve exact pinned captures and verification tools through Git round trips. +* -text whitespace=cr-at-eol +incidents/core-1/core-1/*.log whitespace=-blank-at-eof,cr-at-eol diff --git a/harness/experiments/state-multiselect-20260923/checks/README.md b/harness/experiments/state-multiselect-20260923/checks/README.md new file mode 100644 index 00000000..f9aead6e --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/checks/README.md @@ -0,0 +1,35 @@ +# State multiselect closed gate evidence tools + +Preparation requires an explicit closed run list from `.local/state-group-gates`. +Preserve failed `desktop-1` alongside accepted unchanged runs. Exclude `core-1` +from this package: its false unchanged-input flag and one map/archive mismatch +belong in the separate pinned incident. Neither failures nor incidents are passes. + +Example check-only invocation (extend with every subsequent closed run): + +``` +python -B prepare.py --root CHECKOUT --runs desktop-1 core-2 desktop-2 --check-only +``` + +Only after all selected captures close, omit `--check-only` and provide +`--output CHECKOUT/harness/experiments/state-multiselect-20260923/checks`. +Historical helper copies may be passed via repeatable `--helper PATH`. +No existing output is overwritten. Run `python -B verify.py PACKAGE` for readback. + +The strict verifier is unchanged from the ordinary multiselect package: exact +seven raw files, helper bytes, safe archive inventories, all input-map/archive +bytes equal, receipt relationships and inputsUnchanged exactly true are required. +Failures retain their actual exits and streams. Source capsules preserve local +historical bytes; recorded HEAD alone does not identify those bytes. + +The earlier 733-input helper omitted the core compile-time integration fixture +`harness/experiments/edge-attachments-20260922/native-fixture.arcadia` and the CLI +fixture `harness/experiments/textual-authoring-20260911/alarm-response.arcadia`. +Lint-1 failed, and its all-target capture additionally has a source-capsule +scope limitation for the core fixture. Later 735-input captures add both explicit +inputs. Desktop/core +example captures retain their actual tested scope; do not infer that one map +establishes a later map or complete all-target source identity. + +Offline pin/receipt consistency is not test execution, authentication, native +acceptance, or proof of usability. Executables and toolchains are not distributed. diff --git a/harness/experiments/state-multiselect-20260923/checks/evidence.zip b/harness/experiments/state-multiselect-20260923/checks/evidence.zip new file mode 100644 index 00000000..da79a0f4 Binary files /dev/null and b/harness/experiments/state-multiselect-20260923/checks/evidence.zip differ diff --git a/harness/experiments/state-multiselect-20260923/checks/files.json b/harness/experiments/state-multiselect-20260923/checks/files.json new file mode 100644 index 00000000..2efa857d --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/checks/files.json @@ -0,0 +1,22 @@ +{ + "prepare.py": { + "bytes": 4802, + "sha256": "24f59b1e61a1e9c93d4aa9a47a998cbf9c2058e966cc5a7beceba73e2138943c" + }, + "verify.py": { + "bytes": 8873, + "sha256": "c03513407073bef092d57b968f982b8ffc0c58e095f834fe7464c6a0e8454a9a" + }, + "README.md": { + "bytes": 1984, + "sha256": "157584ed470fb8320d5a03b79388c6e1b7818b25fee1dee913083a41ff847f60" + }, + "manifest.json": { + "bytes": 17487, + "sha256": "d5d71a7352fe8ab8cd63d3ce907c875d98ab048080c28868571819e9afa61e4f" + }, + "evidence.zip": { + "bytes": 8859169, + "sha256": "48a47389ffdba1180b1340c3471f3d8615790e709fe563c39941d95f3f242f77" + } +} diff --git a/harness/experiments/state-multiselect-20260923/checks/manifest.json b/harness/experiments/state-multiselect-20260923/checks/manifest.json new file mode 100644 index 00000000..69438ae1 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/checks/manifest.json @@ -0,0 +1,431 @@ +{ + "schema": 1, + "kind": "multiselect-cargo-gates", + "runs": { + "desktop-1": { + "command": [ + "cargo", + "test", + "-p", + "oa-desktop", + "--message-format=json", + "state_multiselect", + "--", + "--test-threads=1" + ], + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "exitCode": 101, + "inputCount": 733, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "core-2": { + "command": [ + "cargo", + "test", + "-p", + "oa-core", + "--example", + "state_multiselect_native", + "--message-format=json", + "--", + "--test-threads=1" + ], + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "exitCode": 0, + "inputCount": 733, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "desktop-2": { + "command": [ + "cargo", + "test", + "-p", + "oa-desktop", + "--message-format=json", + "--", + "--test-threads=1" + ], + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "exitCode": 0, + "inputCount": 733, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "lint-1": { + "command": [ + "cargo", + "clippy", + "-p", + "oa-core", + "-p", + "oa-desktop", + "--all-targets", + "--message-format=json", + "--", + "-D", + "warnings" + ], + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "exitCode": 101, + "inputCount": 733, + "helper": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "lint-2": { + "command": [ + "cargo", + "clippy", + "-p", + "oa-core", + "-p", + "oa-desktop", + "--all-targets", + "--message-format=json", + "--", + "-D", + "warnings" + ], + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "exitCode": 0, + "inputCount": 735, + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + }, + "format-1": { + "command": [ + "cargo", + "fmt", + "--all", + "--", + "--check" + ], + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "exitCode": 0, + "inputCount": 735, + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + }, + "desktop-3": { + "command": [ + "cargo", + "test", + "-p", + "oa-desktop", + "--message-format=json", + "--", + "state_", + "model_menu", + "--test-threads=1" + ], + "head": "3480b54201a5f361cca0bcd0d45dd6546a7a7bc9", + "exitCode": 0, + "inputCount": 735, + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + }, + "native-build-1": { + "command": [ + "cargo", + "build", + "-p", + "oa-core", + "--example", + "state_multiselect_native", + "--message-format=json" + ], + "head": "3480b54201a5f361cca0bcd0d45dd6546a7a7bc9", + "exitCode": 0, + "inputCount": 735, + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + }, + "windows-build-1": { + "command": [ + "cargo", + "build", + "-p", + "oa-desktop", + "--message-format=json" + ], + "head": "3480b54201a5f361cca0bcd0d45dd6546a7a7bc9", + "exitCode": 0, + "inputCount": 735, + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + } + }, + "logicalFiles": { + "cargo/core-2/inputs.json": "capsules/9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a.json", + "cargo/core-2/inputs.zip": "capsules/3419218f9917de0ac689a32dd622ebe55487fa5a37ffaba4b8fa662bc15a5cb9.zip", + "cargo/core-2/process.json": "cargo/core-2/process.json", + "cargo/core-2/result.json": "cargo/core-2/result.json", + "cargo/core-2/started.json": "cargo/core-2/started.json", + "cargo/core-2/stderr.log": "cargo/core-2/stderr.log", + "cargo/core-2/stdout.log": "cargo/core-2/stdout.log", + "cargo/desktop-1/inputs.json": "capsules/d8a64b62b68ac1c37bc3fe9309abc4dc314e343ce546bba4b9445f68dddeda5d.json", + "cargo/desktop-1/inputs.zip": "capsules/cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e.zip", + "cargo/desktop-1/process.json": "cargo/desktop-1/process.json", + "cargo/desktop-1/result.json": "cargo/desktop-1/result.json", + "cargo/desktop-1/started.json": "cargo/desktop-1/started.json", + "cargo/desktop-1/stderr.log": "cargo/desktop-1/stderr.log", + "cargo/desktop-1/stdout.log": "cargo/desktop-1/stdout.log", + "cargo/desktop-2/inputs.json": "capsules/9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a.json", + "cargo/desktop-2/inputs.zip": "capsules/3419218f9917de0ac689a32dd622ebe55487fa5a37ffaba4b8fa662bc15a5cb9.zip", + "cargo/desktop-2/process.json": "cargo/desktop-2/process.json", + "cargo/desktop-2/result.json": "cargo/desktop-2/result.json", + "cargo/desktop-2/started.json": "cargo/desktop-2/started.json", + "cargo/desktop-2/stderr.log": "cargo/desktop-2/stderr.log", + "cargo/desktop-2/stdout.log": "cargo/desktop-2/stdout.log", + "cargo/desktop-3/inputs.json": "capsules/4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf.json", + "cargo/desktop-3/inputs.zip": "capsules/1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c.zip", + "cargo/desktop-3/process.json": "cargo/desktop-3/process.json", + "cargo/desktop-3/result.json": "cargo/desktop-3/result.json", + "cargo/desktop-3/started.json": "cargo/desktop-3/started.json", + "cargo/desktop-3/stderr.log": "cargo/desktop-3/stderr.log", + "cargo/desktop-3/stdout.log": "cargo/desktop-3/stdout.log", + "cargo/format-1/inputs.json": "capsules/4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf.json", + "cargo/format-1/inputs.zip": "capsules/1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c.zip", + "cargo/format-1/process.json": "cargo/format-1/process.json", + "cargo/format-1/result.json": "cargo/format-1/result.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/lint-1/inputs.json": "capsules/9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a.json", + "cargo/lint-1/inputs.zip": "capsules/3419218f9917de0ac689a32dd622ebe55487fa5a37ffaba4b8fa662bc15a5cb9.zip", + "cargo/lint-1/process.json": "cargo/lint-1/process.json", + "cargo/lint-1/result.json": "cargo/lint-1/result.json", + "cargo/lint-1/started.json": "cargo/lint-1/started.json", + "cargo/lint-1/stderr.log": "cargo/lint-1/stderr.log", + "cargo/lint-1/stdout.log": "cargo/lint-1/stdout.log", + "cargo/lint-2/inputs.json": "capsules/4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf.json", + "cargo/lint-2/inputs.zip": "capsules/1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c.zip", + "cargo/lint-2/process.json": "cargo/lint-2/process.json", + "cargo/lint-2/result.json": "cargo/lint-2/result.json", + "cargo/lint-2/started.json": "cargo/lint-2/started.json", + "cargo/lint-2/stderr.log": "cargo/lint-2/stderr.log", + "cargo/lint-2/stdout.log": "cargo/lint-2/stdout.log", + "cargo/native-build-1/inputs.json": "capsules/4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf.json", + "cargo/native-build-1/inputs.zip": "capsules/1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c.zip", + "cargo/native-build-1/process.json": "cargo/native-build-1/process.json", + "cargo/native-build-1/result.json": "cargo/native-build-1/result.json", + "cargo/native-build-1/started.json": "cargo/native-build-1/started.json", + "cargo/native-build-1/stderr.log": "cargo/native-build-1/stderr.log", + "cargo/native-build-1/stdout.log": "cargo/native-build-1/stdout.log", + "cargo/windows-build-1/inputs.json": "capsules/4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf.json", + "cargo/windows-build-1/inputs.zip": "capsules/1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c.zip", + "cargo/windows-build-1/process.json": "cargo/windows-build-1/process.json", + "cargo/windows-build-1/result.json": "cargo/windows-build-1/result.json", + "cargo/windows-build-1/started.json": "cargo/windows-build-1/started.json", + "cargo/windows-build-1/stderr.log": "cargo/windows-build-1/stderr.log", + "cargo/windows-build-1/stdout.log": "cargo/windows-build-1/stdout.log", + "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py", + "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py": "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py" + }, + "members": { + "capsules/1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c.zip": { + "bytes": 2952517, + "sha256": "1e7f68e8d6156ec608b58ff70f7ddcaf690d38da1d5066092c218bce190d3a4c" + }, + "capsules/3419218f9917de0ac689a32dd622ebe55487fa5a37ffaba4b8fa662bc15a5cb9.zip": { + "bytes": 2948937, + "sha256": "3419218f9917de0ac689a32dd622ebe55487fa5a37ffaba4b8fa662bc15a5cb9" + }, + "capsules/4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf.json": { + "bytes": 123053, + "sha256": "4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf" + }, + "capsules/9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a.json": { + "bytes": 122682, + "sha256": "9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a" + }, + "capsules/cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e.zip": { + "bytes": 2947000, + "sha256": "cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e" + }, + "capsules/d8a64b62b68ac1c37bc3fe9309abc4dc314e343ce546bba4b9445f68dddeda5d.json": { + "bytes": 122682, + "sha256": "d8a64b62b68ac1c37bc3fe9309abc4dc314e343ce546bba4b9445f68dddeda5d" + }, + "cargo/core-2/process.json": { + "bytes": 200, + "sha256": "744543a3f69d993977f0d15bce6e1ab3f64329eda4bb2d80375a8c8794ee83c0" + }, + "cargo/core-2/result.json": { + "bytes": 3196, + "sha256": "4625b4558263524424fe6465aea20d1e9831f4c1cb6298124fa3683957f5880e" + }, + "cargo/core-2/started.json": { + "bytes": 1403, + "sha256": "965d45f368d29ecbea6a7aa9b8881fe18a652928bad8f3e7665443acee52e19b" + }, + "cargo/core-2/stderr.log": { + "bytes": 320, + "sha256": "42a28434514f50c42782a469c68e6aad8644592b9a09c4b50cc43d987bd2fd5d" + }, + "cargo/core-2/stdout.log": { + "bytes": 42721, + "sha256": "862bd3e57a3454d29cb6d9ad7673e5bc4b25ab2a288e15d1a7d8dfb912a1457f" + }, + "cargo/desktop-1/process.json": { + "bytes": 183, + "sha256": "bc57e00984823c7f417c8455f0db3da10f8e453c7db7a808f115b818525227ac" + }, + "cargo/desktop-1/result.json": { + "bytes": 1778, + "sha256": "25f62fde1985dafcf3467895c6a2f389c886e92ceaa9d63ff513f3d4365ffe79" + }, + "cargo/desktop-1/started.json": { + "bytes": 1381, + "sha256": "fd570a96b6dc5fc9e750634ef23093a7205cd953be5a7782485e8dbe509098a6" + }, + "cargo/desktop-1/stderr.log": { + "bytes": 166, + "sha256": "ad39fc56f685f7f3c6a9b4a342abff1f19f9daad28c394673be0e3a2a1cf2523" + }, + "cargo/desktop-1/stdout.log": { + "bytes": 297269, + "sha256": "d1cdbd43e8fe41e4af6fe2d99d665700a78c1d38185d3a373a33895f296e7b93" + }, + "cargo/desktop-2/process.json": { + "bytes": 162, + "sha256": "86efe98400b268723e0c3b38b4bcd270f888215060548181b6f4695774a9b640" + }, + "cargo/desktop-2/result.json": { + "bytes": 3090, + "sha256": "1ae8d0229e429001c2e6214e94e9c4156eb5db75ad1907f421ec87bdeac22f2e" + }, + "cargo/desktop-2/started.json": { + "bytes": 1355, + "sha256": "71356cc6cdcd01d3620a4bd73c8eee2ccbcb870466b7b0419be8e76cd95e4c02" + }, + "cargo/desktop-2/stderr.log": { + "bytes": 288, + "sha256": "5e3681e5c50d0d6b25d9594f87a7767456cefefb35ed0575b5d035202d00070f" + }, + "cargo/desktop-2/stdout.log": { + "bytes": 441409, + "sha256": "f301281be9caa78720540f89548fc1bb80f9b28cda18e4f6425178d4995ed1ec" + }, + "cargo/desktop-3/process.json": { + "bytes": 186, + "sha256": "963dea7f8d8663873ca087b025e53b6555af28a20cce4d5cbdc740c00c4c874d" + }, + "cargo/desktop-3/result.json": { + "bytes": 3307, + "sha256": "ecdb5366bb2ca064924e8e967491a7b3db4adffec5cc8cb3370b7afaf739546b" + }, + "cargo/desktop-3/started.json": { + "bytes": 1573, + "sha256": "5ddbc4ca4954ca732248437cf774cb7803ddec06bef42c11c63b27295be744b2" + }, + "cargo/desktop-3/stderr.log": { + "bytes": 287, + "sha256": "e0d9090ebcc83418a064f8876ec587e4e741a65e380490999c169fa69628fe3b" + }, + "cargo/desktop-3/stdout.log": { + "bytes": 307286, + "sha256": "c91fae7883b55901a2c3ab249650c2bb12a991e744a09c18159ffb3bf0079650" + }, + "cargo/format-1/process.json": { + "bytes": 116, + "sha256": "d316037b78abff45a450b1c116d0c214d1d3831b6c375538779959e1dec652fb" + }, + "cargo/format-1/result.json": { + "bytes": 1871, + "sha256": "2216bb25ce81504c61353456823ec38d77703dd6747f80f2d53b4267799ce21c" + }, + "cargo/format-1/started.json": { + "bytes": 1483, + "sha256": "415e0776ec74ee486f2895e3581a8f78308dda9142d4e542442db9979408788b" + }, + "cargo/format-1/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/format-1/stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "cargo/lint-1/process.json": { + "bytes": 195, + "sha256": "286ba22c1994dfd70834adf811c103057724942f85fa5fbfa22e75e563ac376d" + }, + "cargo/lint-1/result.json": { + "bytes": 1806, + "sha256": "7be8d8e2add4ceb69d5814326e4db91c33e804608e0998323dd72d388cf88def" + }, + "cargo/lint-1/started.json": { + "bytes": 1409, + "sha256": "2fba3385528cf09df0382cbf4ec3867cdd109daad6de3fbfbc3663dbde7a1610" + }, + "cargo/lint-1/stderr.log": { + "bytes": 166, + "sha256": "649dcf5fdf4a79dc2f3b315ef9e6735af44b50c40526c8593d2ce54a834a809d" + }, + "cargo/lint-1/stdout.log": { + "bytes": 271410, + "sha256": "c7085d514e4dbdfc7c3b633911793071e13c27775edfe7ca8cc8f02b70219804" + }, + "cargo/lint-2/process.json": { + "bytes": 196, + "sha256": "d6370e0e1aede61d5cae46028a6a7bab00e25429d1539e0989ef369d318d06c7" + }, + "cargo/lint-2/result.json": { + "bytes": 1988, + "sha256": "0b5458c8567fa43a40c2fcf810999c0bc565d56635ff6fdb9484d79d88c69108" + }, + "cargo/lint-2/started.json": { + "bytes": 1593, + "sha256": "4923e64c03112589eea0c15e3d67f66a94e4fdc1012356f24d67aad94142f386" + }, + "cargo/lint-2/stderr.log": { + "bytes": 207, + "sha256": "d48449d9fa8226c728f9c979d930e91ebd55b84266edd3f7fb308b88a6451009" + }, + "cargo/lint-2/stdout.log": { + "bytes": 309558, + "sha256": "de51e727c694ce4f35e4e732ab820488da053470538f20bdbffd55970eb2caad" + }, + "cargo/native-build-1/process.json": { + "bytes": 175, + "sha256": "30f54028726b906c20cc9b7b0c7c965e3f1e54e6d4d54d99088da6b1c65c2f5e" + }, + "cargo/native-build-1/result.json": { + "bytes": 3311, + "sha256": "03c22f6269b023f77d0fb3e713d5bbab8d14cf6e67ab4dfc613b4691fe4a6955" + }, + "cargo/native-build-1/started.json": { + "bytes": 1552, + "sha256": "340989981b2db65bb194883c1a608f60cacf9a31d36a930dc39da28f5224cb88" + }, + "cargo/native-build-1/stderr.log": { + "bytes": 131, + "sha256": "781b5778b0e781b52c25f5342c9c25d3fc45fe7c0da5bf5c94c86ba80ba7e2d2" + }, + "cargo/native-build-1/stdout.log": { + "bytes": 41724, + "sha256": "5da6c22e3eef843f8be4539a7692e212c0c74461aaf4bf72ce2dd651abe165ac" + }, + "cargo/windows-build-1/process.json": { + "bytes": 137, + "sha256": "2414e8af081cb683edbee3d6b89e5a323b08cce449058d453bffbc9736c1ea12" + }, + "cargo/windows-build-1/result.json": { + "bytes": 3193, + "sha256": "172faec4d7708e4539468c3a9062d424f0e00cb80e827cd579274942c757eccb" + }, + "cargo/windows-build-1/started.json": { + "bytes": 1504, + "sha256": "fefb683ef0e99046f5a18cbad56b8bb8bddc45b8abd322cb2c1d83e2acb26565" + }, + "cargo/windows-build-1/stderr.log": { + "bytes": 137, + "sha256": "f75aa57fe559820a680c771701ea5737995528f29d7dae20e2ae265c07c0b8bb" + }, + "cargo/windows-build-1/stdout.log": { + "bytes": 284179, + "sha256": "04422dbc4d720707d951d0654da39921c0a526b0eb0c33088a3a7e0d85686c31" + }, + "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": { + "bytes": 3827, + "sha256": "1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20" + }, + "helpers/6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06.py": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + } + } +} diff --git a/harness/experiments/state-multiselect-20260923/checks/prepare.py b/harness/experiments/state-multiselect-20260923/checks/prepare.py new file mode 100644 index 00000000..0a6a40f5 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/checks/prepare.py @@ -0,0 +1,90 @@ +"""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): + gates = root / ".local/state-group-gates" + candidates = sorted(gates.glob("*.py")) + helpers + helper_bytes = [path.read_bytes() for path in candidates] + 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 + for run in runs: + folder = gates / run + need(folder.is_dir(), "run directory missing: " + run) + paths = list(folder.iterdir()) + need(all(path.is_file() and not path.is_symlink() for path in paths) + and {path.name for path in paths} == RAW_FILES, "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 + manifest = {"schema": 1, "kind": "multiselect-cargo-gates", "runs": specs, + "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/state-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("--output", type=Path, default=HERE, help="package directory; existing evidence is never overwritten") + 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") + payload, manifest, summary = collect(args.root.resolve(), args.runs, args.helper) + if args.check_only: + print("PASS raw consistency; no package written:", summary) + return + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + 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() diff --git a/harness/experiments/state-multiselect-20260923/checks/verify.py b/harness/experiments/state-multiselect-20260923/checks/verify.py new file mode 100644 index 00000000..2fdb54f5 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/checks/verify.py @@ -0,0 +1,164 @@ +"""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"] == "multiselect-cargo-gates", "manifest schema") + runs, logical, stored = manifest["runs"], manifest["logicalFiles"], manifest["members"] + 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()) + 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]) + 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") + 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), "logicalFiles": len(logical), "storedMembers": len(stored), "sourceCapsulePairs": len(seen_capsules)} + + +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() diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/README.md b/harness/experiments/state-multiselect-20260923/incidents/core-1/README.md new file mode 100644 index 00000000..204429ea --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/README.md @@ -0,0 +1,18 @@ +# Core-1 capture incident + +The original Cargo command recorded exit 0 and eleven tests passing, but +inputsUnchanged is false. Its source archive differs from its earlier input map +only in crates/oa-desktop/src/model_menu.rs (44572 versus 44757 bytes). +This is an invalid capture for normal gate evidence and is never counted as one. +A later unchanged capture must supply the accepted core gate. + +The fixed identity file pins the exact original seven raw files and gate helper, +and the exact two byte hashes for the sole map/archive mismatch. The incident +verifier requires that known mismatch and rejects any other mismatch. It does not +repair or rewrite raw receipts, source bytes, or the false unchanged-input flag. + +Use `python -B prepare.py --root CHECKOUT --check-only` to verify originals. +After review, omit --check-only and supply --output NEW_DIRECTORY. This copies +seven raw files, helper, identity and tools without wrapping a new ZIP. The raw +inputs.zip remains byte-identical. Verify with `python -B verify.py PACKAGE`. +Pin consistency records an incident; it does not prove the cause or execution. diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/inputs.json b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/inputs.json new file mode 100644 index 00000000..13cc50b4 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/inputs.json @@ -0,0 +1,2934 @@ +{ + "Cargo.lock": { + "bytes": 119855, + "sha256": "2aa80d113c5112b31c0767b874845d60f555e4e8ae7eb3ecd0b4b91954b55378" + }, + "Cargo.toml": { + "bytes": 512, + "sha256": "66d401d9f7629f1f6d174ae41b7f9203c9ce3a76d69abfc4381227dbdec12eae" + }, + "build/windows.manifest": { + "bytes": 354, + "sha256": "9c19c5a02f858ece84c44839ad2b145d9c1d90442da0b15c87fd045f975974eb" + }, + "build/windows.rc": { + "bytes": 24, + "sha256": "a27385edcae0bcf96f55361056cb7be5f36d76fcddcaefd20647c91959563e1e" + }, + "build/windows_manifest.rs": { + "bytes": 1460, + "sha256": "168da2c42ecccfebc11ee3e1b455e653a68fe9c6d30a9842a8b9e7441d086c80" + }, + "crates/oa-cli/Cargo.toml": { + "bytes": 279, + "sha256": "49c567f1af74a826683b4d96845feb3806fd9bcb7dc2767e16b5509d4e5e1cde" + }, + "crates/oa-cli/src/lib.rs": { + "bytes": 15506, + "sha256": "790f3bdef78025813262c9677d4db5813f32978a398f3b7fb4900427dc0c314e" + }, + "crates/oa-cli/src/main.rs": { + "bytes": 2984, + "sha256": "de964f9b6c59c0f31c080afbd3b31d442b07b1b44f6a187a248d3409c45d35fb" + }, + "crates/oa-cli/tests/source_cli.rs": { + "bytes": 16600, + "sha256": "a3a1b856c7a9fd3d80bc8074150a24fbb0ef0e9ddf736c7cea036725fc3dc2b8" + }, + "crates/oa-core/Cargo.toml": { + "bytes": 408, + "sha256": "6b5c062474ebb62d5082e07dabed99ee85e4a833edf26690fb4f887028d3416f" + }, + "crates/oa-core/build.rs": { + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + "crates/oa-core/examples/attachment_snapshot.rs": { + "bytes": 1030, + "sha256": "3e0c5581b36a93501c9aed677862bb965165e291eb69a03644bc9758e2e58c9b" + }, + "crates/oa-core/examples/generate_demo.rs": { + "bytes": 795, + "sha256": "e26f432c95331aeca50985dbb3ad2c558affd43023b585811f0f7bfaa59f6661" + }, + "crates/oa-core/examples/generate_release1.rs": { + "bytes": 1798, + "sha256": "1d5f11165d74893e4bc18dbf3a2f2111f4026bd89fd06ee7f0bae69e45c4d4b5" + }, + "crates/oa-core/examples/multiselect_native.rs": { + "bytes": 24234, + "sha256": "26fe5ab5081710fdaecb9a2a3c9be6b518700e5a578f3f930ec64aa4af236527" + }, + "crates/oa-core/examples/release_dense_views.rs": { + "bytes": 14664, + "sha256": "a83158488ef9b7394f136c52b1496719be935c4e5bdfe457cab5c9894f5f9412" + }, + "crates/oa-core/examples/routing_workshop.rs": { + "bytes": 17248, + "sha256": "571307b18b6d4fa04700c5a4d4b0c2154c419e93a21292b2028e7b6f46a2faa7" + }, + "crates/oa-core/examples/state_bend_routing_snapshot.rs": { + "bytes": 4487, + "sha256": "9054f0cdd2c1447de460feb9a325b61f9334c6d09458c24a724ab47f55c3610c" + }, + "crates/oa-core/examples/state_multiselect_native.rs": { + "bytes": 31083, + "sha256": "d85bde2c0780b9a95a6dfed425efc04513b2ee3330b3c637f9bd471937ee0972" + }, + "crates/oa-core/src/analysis_numeric.rs": { + "bytes": 3735, + "sha256": "6c88eef3119162aeb6c07b9e75812c3af9fbfe75554b0503ad00f103cb478408" + }, + "crates/oa-core/src/analysis_numeric/dimension.rs": { + "bytes": 3045, + "sha256": "d4ad8e5307a1620e31791d724a0e31fdba860d00dc83e3e1f2209d9c5e22a340" + }, + "crates/oa-core/src/analysis_numeric/exact.rs": { + "bytes": 17338, + "sha256": "4eb8cb0f66b10e5da204899af82ee26adfec74bab47b1a6c9842b5488ee04c4d" + }, + "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json": { + "bytes": 6085, + "sha256": "3b959dec5e100a6f03817f5314bef497347ba579ebd235476516d4bd83399181" + }, + "crates/oa-core/src/analysis_numeric/meter.rs": { + "bytes": 7129, + "sha256": "1f7174beb992935b4c399acdef91114af73faf041070feab968455ac827e8ee4" + }, + "crates/oa-core/src/analysis_numeric/quantity.rs": { + "bytes": 12257, + "sha256": "ead5142d6eeea8f99399d57e02d8f7d819980923de76387cd454694d6a747aee" + }, + "crates/oa-core/src/analysis_numeric/tests.rs": { + "bytes": 27486, + "sha256": "323ce82fc057d36f44463569fd8ea324c7d700fddfbf32bce15e04bd2a66970b" + }, + "crates/oa-core/src/data.rs": { + "bytes": 42696, + "sha256": "7f9c7c7dd8353cb5a38597b92cde6d3e890203d392473187eb387d41ff37414a" + }, + "crates/oa-core/src/demo.rs": { + "bytes": 20657, + "sha256": "ad62123f96c20ff023cafc1a9849b0fa06bc62edd1fb9701799e4599b458600c" + }, + "crates/oa-core/src/diagram_edit.rs": { + "bytes": 3412, + "sha256": "7f307d64fe09947973871a05bc8d5dca82b64b65a8e6b3aaed22d6efc6497962" + }, + "crates/oa-core/src/language.rs": { + "bytes": 107446, + "sha256": "ec20045608ff94f6ad1dff51507f2ced8f1b1aa86f2a85900c8cbcb2117a53b4" + }, + "crates/oa-core/src/language/semantic_source.rs": { + "bytes": 7326, + "sha256": "584b5bb59d87a7b8d44d5dd2d124c3a1323b21492b138a17bc9526c0087c9e29" + }, + "crates/oa-core/src/language_nesting_tests.rs": { + "bytes": 20639, + "sha256": "bd061611e8cb6f28ab203c73e544ac98bf275eb464cd314ad5fbd25f773df33b" + }, + "crates/oa-core/src/language_outline_tests.rs": { + "bytes": 14877, + "sha256": "4c79a01e8aea243dd9ef66ce71b9fae263d0e79c319a46c51223679897e8b9df" + }, + "crates/oa-core/src/lib.rs": { + "bytes": 592, + "sha256": "1a933305e945239d1daf69347c93861e286c1e0f7f4b29893b37a74a764c987b" + }, + "crates/oa-core/src/migration_v2/README.md": { + "bytes": 1515, + "sha256": "fb61d3faa33d7bdfba73415415a0882c0fbd3e0b45df42f972a19aec6da927b9" + }, + "crates/oa-core/src/migration_v2/adapter.rs": { + "bytes": 9490, + "sha256": "66741b4c8354a23b8a932ecc5bf1bc3bb6e74279558757b8c35bc2157a7e51b5" + }, + "crates/oa-core/src/migration_v2/mod.rs": { + "bytes": 1158, + "sha256": "857d8bb13b6814daba6780ca770c16352943c01cb130245d055f11b3b957b3e9" + }, + "crates/oa-core/src/migration_v2/model.rs": { + "bytes": 52059, + "sha256": "8ac63eb59b2e57bdecff2477ee0634096090924d3dd1851e1c454880a889dacb" + }, + "crates/oa-core/src/migration_v2/references.rs": { + "bytes": 13369, + "sha256": "33a6819abc83139fdaebeebd1b553238a328637106cce7dd5a535f1791d43e20" + }, + "crates/oa-core/src/migration_v2/source.rs": { + "bytes": 21946, + "sha256": "74fee9b24e7c912182c998ab9d9aeec125e2009c3214959229e844c67afe2bf6" + }, + "crates/oa-core/src/migration_v2/state_validation.rs": { + "bytes": 15769, + "sha256": "e29ef071bab0a68460ddf55c46131c6f66c022d52c3c602c37c013c3649da1b0" + }, + "crates/oa-core/src/migration_v2/transitions.rs": { + "bytes": 704, + "sha256": "f74174c532ba56b57c80b719a11fb4adea0a78ccc2828fff72fe2b9eda5a10b1" + }, + "crates/oa-core/src/migration_v2/validation.rs": { + "bytes": 98736, + "sha256": "6d5e4a4e2805ae5045bf789a0485e14a61c63346587bace2b4a91173a688f780" + }, + "crates/oa-core/src/migration_v3/README.md": { + "bytes": 2128, + "sha256": "3755a63f840d7f0eadf68afa7a9d587877a9468ff982d3610539e8a0bb067a82" + }, + "crates/oa-core/src/migration_v3/mod.rs": { + "bytes": 23023, + "sha256": "f4edb6a0b50ecfe8670973776d703047b064420cb1f05913129c00fc656b3722" + }, + "crates/oa-core/src/migration_v4.rs": { + "bytes": 4020, + "sha256": "50c652964f0f309e7f3f54b6ef12c40d42fcecc0490f005995c713df1179af70" + }, + "crates/oa-core/src/model.rs": { + "bytes": 67431, + "sha256": "8bcb2bc7046cb52d9703028a11ca8e7e9ca890411bd444351326ab32262be537" + }, + "crates/oa-core/src/operations.rs": { + "bytes": 11405, + "sha256": "a5ffbbf54cd440316a106d1188bfb699b57dae59f466edcb33618c333cd19f41" + }, + "crates/oa-core/src/persistence.rs": { + "bytes": 31774, + "sha256": "1c01474f906a63e552c0d590b272c95821c00a100b3e79e58e67dbbeac341a14" + }, + "crates/oa-core/src/ports.rs": { + "bytes": 45413, + "sha256": "5cac12eea70a54ebebbd6be77b63ce92e3195325c4bc7d27705c518686cbac80" + }, + "crates/oa-core/src/projection.rs": { + "bytes": 21374, + "sha256": "69ed12bace2cc5024e151752a174ab14034ac2d05eb741fcf13e9a27521c059b" + }, + "crates/oa-core/src/projection/port_layout.rs": { + "bytes": 13474, + "sha256": "a08d17d10a58bb8b206dd001b19b91d5aa3a9f743b298a4da57d503223c20079" + }, + "crates/oa-core/src/projection/state.rs": { + "bytes": 5233, + "sha256": "73a607bac713ec7ff49a509fefb1a53c7686dca93ee241ffc835564d743afb28" + }, + "crates/oa-core/src/references.rs": { + "bytes": 21957, + "sha256": "93553c234f32da35f1f41a7b83ad81c471c9803efa78ca222cf7de3c2f86c0bb" + }, + "crates/oa-core/src/routing.rs": { + "bytes": 53958, + "sha256": "b377fe80cece3892d64f62ba5e1578d8974eeefc2a317620f343a86f2f257fbd" + }, + "crates/oa-core/src/routing/attachments.rs": { + "bytes": 5918, + "sha256": "cfb4200cdb99e4d86eb6e5ac1b123e4075fb7d4c788860d154365ad8ece139d7" + }, + "crates/oa-core/src/semantic.rs": { + "bytes": 540, + "sha256": "f9895dd5b8fb27d10d4cfbb41c3542ef2018365a589b8d5778e5674ae5a1f239" + }, + "crates/oa-core/src/semantic/catalog.rs": { + "bytes": 49407, + "sha256": "1ff74fcca18084d178681d7ff38ee1ccac632c27c4de0595ba1ceab8067ab3a4" + }, + "crates/oa-core/src/semantic/historical.rs": { + "bytes": 15271, + "sha256": "eaf3284bf234312698514cdb636bc8348e1b5d746cb16b18df777e77d312abfb" + }, + "crates/oa-core/src/semantic/linked.rs": { + "bytes": 6035, + "sha256": "6f335b14e809aecfc35defcaf77cb821b080d1506dbc46297912b25af98c07a2" + }, + "crates/oa-core/src/semantic/mutate.rs": { + "bytes": 22683, + "sha256": "dc2b4c4a6eb156fe468eaa8c85fd37a505c0cd8ea1f226cb69cd01689afd3d9d" + }, + "crates/oa-core/src/semantic/review.rs": { + "bytes": 51116, + "sha256": "0c157d2c327e72e89cca3b423e71ee73798105c5bb7271439c1a101642c91e3e" + }, + "crates/oa-core/src/semantic/types.rs": { + "bytes": 10405, + "sha256": "6922fe35089f12fad43ab0ce1c6886d01a3855f90b8e2543e99802ac5e6f1bbb" + }, + "crates/oa-core/src/session.rs": { + "bytes": 35237, + "sha256": "1eb77b329b2986fc6f5dc0171e77fd860375cdfd8c769a1367b445d6c49ebe00" + }, + "crates/oa-core/src/source_defaults.rs": { + "bytes": 15975, + "sha256": "a18bac93369c84dedb965cd2245bf38997488574452d4ef1de681b91794c8254" + }, + "crates/oa-core/src/source_defaults_tests.rs": { + "bytes": 25751, + "sha256": "160c0ca3725e07777f87e1f82182e09cd1cf47f1aef37961443bf5a714aa660d" + }, + "crates/oa-core/src/source_files.rs": { + "bytes": 75065, + "sha256": "bb903f8c53456f69c5ac213a192a1eb0d19abf8344bd82a03be8e659ba7bc94f" + }, + "crates/oa-core/src/source_project.rs": { + "bytes": 66734, + "sha256": "ee925b142c3072e2ad800922f43b6569a5c8537a037a6c0cd7533d9b9edefa6b" + }, + "crates/oa-core/src/source_project/source_migration.rs": { + "bytes": 4813, + "sha256": "424d4a3dbdc77375536192c94847f90df9f1a3d2fa1c5f59ecbc6387b648f940" + }, + "crates/oa-core/src/source_project/source_structure.rs": { + "bytes": 20225, + "sha256": "9f1f83315856a7025fe662c0fbb13452f31613224a03663150d6225d1ddc162f" + }, + "crates/oa-core/src/state_validation.rs": { + "bytes": 15665, + "sha256": "e28d7f74385e2a78763716b34491bd73a00bfeccc085970ceb130627e3faee18" + }, + "crates/oa-core/src/topology_context.rs": { + "bytes": 13734, + "sha256": "16de69b297de95d93c73808621607833b713af811dd082f0169186ae1e5c3e2d" + }, + "crates/oa-core/src/topology_context/analysis.rs": { + "bytes": 20036, + "sha256": "dead6194f0aa60cc46045c0e5ee10dbdbb689158c38634dfbbbc45d2ba88ac65" + }, + "crates/oa-core/src/topology_context/analysis_tests.rs": { + "bytes": 5258, + "sha256": "17839263410815e96fe027d4eba3aae0a9ab3b82b16947e2b29ebe0d09782b56" + }, + "crates/oa-core/src/transitions.rs": { + "bytes": 51998, + "sha256": "fadd1b07b89cae4117b91abb4f1ac9ea81ff818ef20f2175e76ee8cf21fb708d" + }, + "crates/oa-core/src/transitions/communication.rs": { + "bytes": 8634, + "sha256": "2b476630a88edd9021b9156ca12a5402e01604095f0d4002afb452671903a028" + }, + "crates/oa-core/src/validation.rs": { + "bytes": 113370, + "sha256": "5266c71a378333529d558903c01486763adece40b503ef5f8db37dfc9942e79b" + }, + "crates/oa-core/src/validation/coverage.rs": { + "bytes": 6478, + "sha256": "b08e4b344ecad055714168c83beff7fe29ba4edc53edbffa2eae04e5580a4d5f" + }, + "crates/oa-core/src/view_options.rs": { + "bytes": 24481, + "sha256": "421ede0a04c593ca1df0e6e0d06fabcfcc8113cdbcfb0adc7f5c8ef4d1724208" + }, + "crates/oa-core/src/view_options_tests.rs": { + "bytes": 21433, + "sha256": "64aac44de2df42a1012239a3dd75c473daf0c8990b3170b7bf7e78a556080208" + }, + "crates/oa-core/tests/attachment_geometry.rs": { + "bytes": 13906, + "sha256": "4367f5fe0c8aee8ccd1b66f16327e1d20fe9434d733d7b2ffc281d2f97f685e0" + }, + "crates/oa-core/tests/attachment_lifecycle.rs": { + "bytes": 8400, + "sha256": "720b71c0092b8506fa73f230de117f658cd5ce095dbf40217c0070ad65e651f0" + }, + "crates/oa-core/tests/attachment_trace_ports.rs": { + "bytes": 4817, + "sha256": "b27f9c4029bbd8fc1966449e1f968aa6f7f217ca71f4d464955cecacaa51ab86" + }, + "crates/oa-core/tests/attachment_workshop.rs": { + "bytes": 4270, + "sha256": "82a451dd96d806e88bfbcb363037898e054214210bca0e20e025415b9072a446" + }, + "crates/oa-core/tests/communication_boundary_scenarios.rs": { + "bytes": 8816, + "sha256": "e1100b8f363910a487d90c73acc7df543dc6d532158c3c63ad11cd3d40d8e12b" + }, + "crates/oa-core/tests/communication_means.rs": { + "bytes": 18102, + "sha256": "a25f2749180bc6f4beb1445bfad3351eedf0669131a7439b633ab804439e95a4" + }, + "crates/oa-core/tests/coverage_diagnostics.rs": { + "bytes": 20527, + "sha256": "e2e9c3996daab47f2bd23f5d8c48b4e372af33331152f5fc85b76f06d04fcdf4" + }, + "crates/oa-core/tests/default_omission_admission.rs": { + "bytes": 10733, + "sha256": "11ea431453c34f57e3d45b2c446a773821f0875a19d1d17ae5a6362d5f4c2c82" + }, + "crates/oa-core/tests/default_omission_fidelity.rs": { + "bytes": 16002, + "sha256": "93cad594b124d7a0e91b1cb5c2671643e44e34fe6f31e93c1944e8fd7b89c17a" + }, + "crates/oa-core/tests/default_omission_output.rs": { + "bytes": 9529, + "sha256": "1da3133853c31f6bfe8fa7fc47ba3050ddfdd3c2a112315ae77d16108763a7b5" + }, + "crates/oa-core/tests/default_omission_preservation.rs": { + "bytes": 20792, + "sha256": "873f6471963ff3c5c8b97a6a570fe61f60a03a42363c4c504e39f5caa491ab07" + }, + "crates/oa-core/tests/default_omission_updates.rs": { + "bytes": 8090, + "sha256": "43fefab9149d2d1f8a325a5de20cc67a3423debe4e245348cb5e05b9ffc14b03" + }, + "crates/oa-core/tests/edge_attachments.rs": { + "bytes": 10650, + "sha256": "32d8d96c6edaedbcc8479af2af7f78952f8aab4c2fe8b5fd5608f0031edb2eaa" + }, + "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia": { + "bytes": 29220, + "sha256": "f0df129c1914bfc01d0b047b0cd4b520d8cf0c249ea9ced085de400c0be87ff3" + }, + "crates/oa-core/tests/fixtures/v2-coastal-rescue.json": { + "bytes": 43990, + "sha256": "3ed53508eb544bbd788832f7bebea2ec31b3d66191f24f45098cc9750ec4c005" + }, + "crates/oa-core/tests/fixtures/v3-data-contract.arcadia": { + "bytes": 2450, + "sha256": "305f19d1b79f99f8d4cf627ba2e9ca6836bb7959cc0070227815937a699eafea" + }, + "crates/oa-core/tests/fixtures/v3-data-contract.json": { + "bytes": 8240, + "sha256": "1b41299c53204e6232397d25a58d5bccdbbc16e770c75e8edb55ce36ccfe58a2" + }, + "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia": { + "bytes": 1222, + "sha256": "5fa53a26a5ad010c8e59eb56d9b2b52003569e66c0377ffb58840590168ad0c1" + }, + "crates/oa-core/tests/fixtures/v4-reviewed-prose.json": { + "bytes": 1734, + "sha256": "2808088afcc165f4bd6da5129151143411c997041c3de61d600ab76e05d24a47" + }, + "crates/oa-core/tests/group_movement.rs": { + "bytes": 15221, + "sha256": "a6c7b4c1b2137da923474f5e43faf72f24fe681fd36b4752a42fa45f92297d04" + }, + "crates/oa-core/tests/language.rs": { + "bytes": 57451, + "sha256": "9ad8bd0d3804e75e1aefedb93895053e1ef27647c6169bc38a33a7d5950d7801" + }, + "crates/oa-core/tests/port_anchors.rs": { + "bytes": 27347, + "sha256": "69f6fd78aa74a22f6ad40042c69f219e2e53beb83a1a6a60ca545ce86a16f9d5" + }, + "crates/oa-core/tests/port_migration.rs": { + "bytes": 15620, + "sha256": "1801c376a5435ac7efb2a8342348b2c7ffef930bba65deca4acb66b06e0483d7" + }, + "crates/oa-core/tests/port_source.rs": { + "bytes": 4858, + "sha256": "8d2ddaca4045d330f6eda2591561497b858aad53769a823249d0bea4f5916e12" + }, + "crates/oa-core/tests/prepared_commands.rs": { + "bytes": 9555, + "sha256": "52cab1c668064777412df0a6a3c20d74d579b33794b4b00525b9f151c06eac9b" + }, + "crates/oa-core/tests/process_diagnostics.rs": { + "bytes": 1104, + "sha256": "1dbbc590239ef7542be4a2bdc9543aaa64d108417b03dd312a0894e364745906" + }, + "crates/oa-core/tests/r3_constructor_ack.rs": { + "bytes": 24163, + "sha256": "e5c44676ea0aab97f2e091ddac2f4bc4a4a90f5877f7bbf08b61229600944dd0" + }, + "crates/oa-core/tests/r3_migration.rs": { + "bytes": 6590, + "sha256": "ebd04120f5215b3956fdc79ee78f6e38734c7d13392f1fc28aaba957912265bd" + }, + "crates/oa-core/tests/r3_semantic.rs": { + "bytes": 77473, + "sha256": "49c3763eaf2131070a7c46cc23a84f917d292d41610b6404510e5607f8890c32" + }, + "crates/oa-core/tests/r3_source.rs": { + "bytes": 15311, + "sha256": "f31f60659df3c7ef601650d1cff140589d8729f01a3f65a4a6a6c2272f15c408" + }, + "crates/oa-core/tests/r6_data.rs": { + "bytes": 41437, + "sha256": "5f7ffb3bc73c3c85423623fe56f8680646dc97040327bdbead9d47a37e80f155" + }, + "crates/oa-core/tests/r6_migration.rs": { + "bytes": 9174, + "sha256": "81519bc1512f5e069432e9a1cdad04c10216d492df01b4b75b02102480ebd6a1" + }, + "crates/oa-core/tests/r6_source.rs": { + "bytes": 9519, + "sha256": "88a0aa087a7678e2898ff6057538072ca92072190712f05fff08372cc56ebed6" + }, + "crates/oa-core/tests/release1_teaching.rs": { + "bytes": 10010, + "sha256": "be179290d871125dc97cfd2a3067c7ac89c8a538fdb0afafc5b825bcdfb2bef2" + }, + "crates/oa-core/tests/release_capabilities.rs": { + "bytes": 19422, + "sha256": "b92cba2a562e78287ddd7dc3e94557848d17faa0c8a102601b50286b9af48a30" + }, + "crates/oa-core/tests/release_diagrams.rs": { + "bytes": 7689, + "sha256": "cefcc0582d91266d5de6ee5daefcfe41b606ab971ab235b59034a8e1dbc24b7d" + }, + "crates/oa-core/tests/release_language_limits.rs": { + "bytes": 5204, + "sha256": "8578da4f76bbd2d0e1bfe295ab1259e2162a3fe3d0d2ba8901915d7a40942806" + }, + "crates/oa-core/tests/release_ports.rs": { + "bytes": 32740, + "sha256": "506e62beab0c7ec0cece83e9e0c1a810fa1f99285c9e1cab0c77f06de55a40e1" + }, + "crates/oa-core/tests/release_routing.rs": { + "bytes": 23305, + "sha256": "daf75dedd76c9bd710d7859cb80759dd243383f40970fe7dad39c706f01289e5" + }, + "crates/oa-core/tests/release_scenarios.rs": { + "bytes": 41067, + "sha256": "0f5d876badeec653fc7b12610a77d867e9a53582e605253deff8d379da21572f" + }, + "crates/oa-core/tests/release_states.rs": { + "bytes": 26873, + "sha256": "b1a26acee95eaba7f9920f9da738a1e33606552eb078e6d298895daff7e3d811" + }, + "crates/oa-core/tests/release_topology.rs": { + "bytes": 17643, + "sha256": "c51dbe5aa5a038f9ed830daa02aa67714472179eb7fd6da5ce36dc6931c8b3a9" + }, + "crates/oa-core/tests/review_regressions.rs": { + "bytes": 21823, + "sha256": "f154e5c88d35e31512e2b07733a1327ea3e2fcfd1ef8eb23066a4e9573f387c1" + }, + "crates/oa-core/tests/route_source.rs": { + "bytes": 7270, + "sha256": "1a73fdbac0d0ae0e84d8d67b6ad398e11223958f5dca007f73757c91827decfb" + }, + "crates/oa-core/tests/routing_geometry.rs": { + "bytes": 31956, + "sha256": "d3bd79e0d8dcc2445495b56a474e52eb62be510b679ea290569cce16ea5ebc94" + }, + "crates/oa-core/tests/routing_labels.rs": { + "bytes": 5604, + "sha256": "2d5529a8994d645627c6dd3f7e560a8fe42313c569db487ca213248852c6addd" + }, + "crates/oa-core/tests/semantic_artifact_absence_oracle.rs": { + "bytes": 4640, + "sha256": "d93eaf9667f4221232168bef0fe9f069e5be1b1f23410bda5dd67010b96e5c8a" + }, + "crates/oa-core/tests/semantic_attachments_gui_oracle.rs": { + "bytes": 14957, + "sha256": "7f526d363ba05e7ac89faf0fead0796e419c34ee204edf44a1b46c78308b0970" + }, + "crates/oa-core/tests/source6_view_contract.rs": { + "bytes": 14307, + "sha256": "fa566de53946d40bbbd9033edfeed177180c4dc44afa2f2f4667ffdc622eb3bb" + }, + "crates/oa-core/tests/source7_contract.rs": { + "bytes": 43262, + "sha256": "cad4f1a135ddbd947bdf3c71d1b44f1e438725095ea8c4d893464bc721c2363f" + }, + "crates/oa-core/tests/source_files.rs": { + "bytes": 16797, + "sha256": "7566e2b44e19e6fd201e2460aa09a710992f6e208bdd55fb2db79e3797d9e568" + }, + "crates/oa-core/tests/source_identity_updates.rs": { + "bytes": 5139, + "sha256": "f52aa38b4d317e532c641ad7463472e4b40e802e60052cf0017556ffc63da06c" + }, + "crates/oa-core/tests/source_nested_updates.rs": { + "bytes": 16892, + "sha256": "9ac3390936c0d250651ea94a1b8faf0adfb680f061ececc56f2a52909b5ba855" + }, + "crates/oa-core/tests/source_project.rs": { + "bytes": 45910, + "sha256": "cef58adb16aa4d2527ddc0628aec3e26f0b42b4be70014a7e360fafdac13a942" + }, + "crates/oa-core/tests/state_diagrams.rs": { + "bytes": 24525, + "sha256": "9d8acf03419c3a85f2f6288104e0d0f1f802b36dabcd2c4c05f445605d3eabbd" + }, + "crates/oa-core/tests/workflows.rs": { + "bytes": 33426, + "sha256": "bd843dfddbd5724ec08f657571f887496203056fe2fa5864ef50a168a613b89b" + }, + "crates/oa-desktop/Cargo.toml": { + "bytes": 874, + "sha256": "e4fc5d2c14d9aa84ec493d0c6bef47a1046b4929ca502e4bd096c27d4fe22587" + }, + "crates/oa-desktop/build.rs": { + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + "crates/oa-desktop/src/allocation_drag.rs": { + "bytes": 29634, + "sha256": "6b93290627a8697dd529ecf12b6e99196a11783afff1ca7aaf75c6e2762f3d94" + }, + "crates/oa-desktop/src/allocation_drag/tests.rs": { + "bytes": 60377, + "sha256": "0d493bec942fa3d214a8a28648f8cf9c094f607da390457657af0c80727efb62" + }, + "crates/oa-desktop/src/allocation_input.rs": { + "bytes": 8526, + "sha256": "7ceb868b158e4384b217b2b2b118c3714fc6d851fa2c89c2a61236cae125ffda" + }, + "crates/oa-desktop/src/app.rs": { + "bytes": 259249, + "sha256": "9828e55e877f782f024135f11df8b28d88014d97344951e2af9f36d4536e1b5f" + }, + "crates/oa-desktop/src/app/allocation_drag.rs": { + "bytes": 25621, + "sha256": "5b718d8f8af151cb04ed6f2491186911cdf2303267caab88dbfae9fb861b8286" + }, + "crates/oa-desktop/src/app/allocation_drag_full_tree_tests.rs": { + "bytes": 28288, + "sha256": "ff4c3d07f885f242b71160c09e2c457c4a746073fd89030781da306730c5db44" + }, + "crates/oa-desktop/src/app/allocation_drag_tests.rs": { + "bytes": 42846, + "sha256": "a964162cac74e6a6b001a400f92be3d83a55c28ca01fac84f6e62bea698e7eaf" + }, + "crates/oa-desktop/src/app/allocation_drag_tests/navigation_union.rs": { + "bytes": 7252, + "sha256": "7cb14e0ad1b300c926635a89cda8243a54fe533dfa68b959992abe423b31f50b" + }, + "crates/oa-desktop/src/app/architecture_authoring.rs": { + "bytes": 27835, + "sha256": "37c7db33e99dd4adcc5a51f09ee4878af94fd32413359890a541690080885f65" + }, + "crates/oa-desktop/src/app/architecture_authoring/palette.rs": { + "bytes": 8887, + "sha256": "8f9937cb30b48b83759fd084a8a5990a2daa3920280f1210abe048560241985a" + }, + "crates/oa-desktop/src/app/architecture_authoring/tests.rs": { + "bytes": 14657, + "sha256": "18f4e2d635ee6d5783b9230783a4fb3191818dab27c85d732f65be976ad5126a" + }, + "crates/oa-desktop/src/app/architecture_authoring_tests.rs": { + "bytes": 18735, + "sha256": "8f18b6f30cd1acc1520eac98b423eae53234b6719ca7da1f139ae97b2180cf94" + }, + "crates/oa-desktop/src/app/behavior_lifecycle_tests.rs": { + "bytes": 13672, + "sha256": "f9a0b030faca5deaf3f55d9f5efcafdf1a7cb1a663001aa20a61baba4f92d080" + }, + "crates/oa-desktop/src/app/behavior_removal_tests.rs": { + "bytes": 23958, + "sha256": "ef09843a7daeb91afab0b76e83c5679351a1635a84a26d80a194dea744ea5874" + }, + "crates/oa-desktop/src/app/checkpoints.rs": { + "bytes": 29576, + "sha256": "55e5babdcabef767af3bc8be9cc3153b3f5cc0695701b890820ec0225643852e" + }, + "crates/oa-desktop/src/app/diagram_focus_tests.rs": { + "bytes": 22032, + "sha256": "106b6d530c30fdef877559a8dbb4ce9cb37c9fe4d208c5dd0aaaa8024e3b1857" + }, + "crates/oa-desktop/src/app/diagram_removal.rs": { + "bytes": 8778, + "sha256": "addb51604f41d680b06586bb362362ee3c2603a334f33022ce9abc281295ac85" + }, + "crates/oa-desktop/src/app/diagram_removal_tests.rs": { + "bytes": 47675, + "sha256": "3062ce2ad0498c6691a5bdf46bd7ec69781d964df2fa33db7bb403ccbb56a8f3" + }, + "crates/oa-desktop/src/app/diagram_removal_tests/navigation_union.rs": { + "bytes": 5542, + "sha256": "7396e60a11d3ef2147111e56d0cab107685032be30c7a22143a1dadae7548101" + }, + "crates/oa-desktop/src/app/diagram_reveal.rs": { + "bytes": 6007, + "sha256": "a8f341084c973708ffff6ee38a3f92c74c7e5301bad664bcdb3b3a4400261e0f" + }, + "crates/oa-desktop/src/app/diagram_reveal/tests.rs": { + "bytes": 14734, + "sha256": "626796a5e162beb855c007b0a5ab1508f49ebc782a4ddcf9f9b6ec84f7231724" + }, + "crates/oa-desktop/src/app/diagram_selection_tests.rs": { + "bytes": 5582, + "sha256": "4fa507adbc23db3a6eb202d809c695ec0ebd6181356930c4fa7528a9a0c7be57" + }, + "crates/oa-desktop/src/app/diagram_usages.rs": { + "bytes": 18334, + "sha256": "db03ed7346fa8f08bdea936454b1e152c4492bf00d98b5a02c853db11153c62f" + }, + "crates/oa-desktop/src/app/diagram_usages/tests.rs": { + "bytes": 50934, + "sha256": "c1fb39aba21d374af4dcf24db6716befb8d465142263991230195fa3805feac2" + }, + "crates/oa-desktop/src/app/hosted.rs": { + "bytes": 25885, + "sha256": "08346c39a094f38d58a2a2f33ff8032608bbe4ad862a35e7fa5a39ecfbbe0f4c" + }, + "crates/oa-desktop/src/app/hosted/tests.rs": { + "bytes": 30344, + "sha256": "fa118f64d04b400ccc01e4e67adaf3541975288882afa265a9e7f092cdbda232" + }, + "crates/oa-desktop/src/app/multiselect.rs": { + "bytes": 15752, + "sha256": "1b1c29c2153aa976272e38136433731b6c3b53f0610ab53f03341ec664f8b361" + }, + "crates/oa-desktop/src/app/multiselect_tests.rs": { + "bytes": 22964, + "sha256": "d1bb7eb7e4f96744fc23bbdd5dec73aec59fe94bc3503317936cf0dfe421b082" + }, + "crates/oa-desktop/src/app/process_canvas.rs": { + "bytes": 31560, + "sha256": "7e05f4899a0b24df9ab355b6484e94997c7bc66c873342d975aed0b6a56acf04" + }, + "crates/oa-desktop/src/app/reference_authoring.rs": { + "bytes": 11250, + "sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77" + }, + "crates/oa-desktop/src/app/reference_authoring/tests.rs": { + "bytes": 20067, + "sha256": "b96ddc6bff7f8fde97720e18a92070f891c1340348d2e19c7c9c5a7bda650ce8" + }, + "crates/oa-desktop/src/app/reference_authoring/tests/mounted.rs": { + "bytes": 32805, + "sha256": "14a4914a1c86e50c395672dab8fe80d7af679a79c9082890b509fad7b7c543d9" + }, + "crates/oa-desktop/src/app/repair_save_state_tests.rs": { + "bytes": 10745, + "sha256": "5543dcba853bb1513acf824b860249729a6004747e7e320b46efe60ea19e2d83" + }, + "crates/oa-desktop/src/app/review_navigation_tests.rs": { + "bytes": 7049, + "sha256": "872fa91479dad5436e139ffcaece270323e9f5daeeb7a4d80c2c2534f48bc21b" + }, + "crates/oa-desktop/src/app/save_as_tests.rs": { + "bytes": 4113, + "sha256": "f8b915793f1a36fd152569c4781a4bdc17f4af404a8ceb373330053413087a1c" + }, + "crates/oa-desktop/src/app/source_rename.rs": { + "bytes": 47151, + "sha256": "724fa01aa490d468742b0b85ebb05f2074e57d1992cbf8855575d2f2be83bfff" + }, + "crates/oa-desktop/src/app/source_rename/alias_input.rs": { + "bytes": 6940, + "sha256": "5888b50e4d6fabd8cd0c6d70986a791d8e95f190453ea37ebb251f2fa19cda90" + }, + "crates/oa-desktop/src/app/source_rename/alias_input/tests.rs": { + "bytes": 7279, + "sha256": "f9dd619c440a6d784792be5e41bad562ef312e07967d193b0e96cdc127debfb2" + }, + "crates/oa-desktop/src/app/source_rename/editor_patch.rs": { + "bytes": 11423, + "sha256": "049c9774c1ca28cda01f9a8e024604bef282366aa52c53c17c1918dbd09c3441" + }, + "crates/oa-desktop/src/app/source_rename/navigation_union_tests.rs": { + "bytes": 17199, + "sha256": "1c1152214abfef70841e9580d21178e1fa40a984f3d6c97afb71a1adea1eadef" + }, + "crates/oa-desktop/src/app/source_rename/navigation_union_tests/palette_union.rs": { + "bytes": 12186, + "sha256": "14cc9fc555243de0e8c97b6b662d69f569e7c58137c11108cc0388e9842f3ba5" + }, + "crates/oa-desktop/src/app/source_rename/preview_glyph_tests.rs": { + "bytes": 16247, + "sha256": "d45a799bd97047c6b77ff5b18334f2a71912b65d5239f2b8ce7017588ccff0f4" + }, + "crates/oa-desktop/src/app/source_rename/preview_scroll_tests.rs": { + "bytes": 21447, + "sha256": "bdd9580bdb3cf2c8f41b92e5d68a79faaa99c0a5a9d72490d4aef07513fd5e13" + }, + "crates/oa-desktop/src/app/source_rename/tests.rs": { + "bytes": 54633, + "sha256": "9eb3011a55f564346236736568c7967d12a3949995496b9d95cf3ddadc220193" + }, + "crates/oa-desktop/src/app/source_rename_layout_timing_tests.rs": { + "bytes": 12253, + "sha256": "dadf8ce0ec989b9a370703a330bde4b415a813273917037fc6a732b9bf2f98ce" + }, + "crates/oa-desktop/src/app/source_rename_timing_tests.rs": { + "bytes": 13912, + "sha256": "942753946d00cab9d4a19717bec5f429af58c070e2cd1b97cb4d32ed6fb93967" + }, + "crates/oa-desktop/src/app/source_symbols.rs": { + "bytes": 26369, + "sha256": "9dbcb70a7eaa0b2f7ec011da1fcc808ba1e53ec1c29a3c9a09cf7517e058c7b7" + }, + "crates/oa-desktop/src/app/source_symbols/focus_selection_tests.rs": { + "bytes": 18552, + "sha256": "a7de38f793d31a7a022b18ce74aa10f5decdf537fea8d9d61e5e61a6bb4f125b" + }, + "crates/oa-desktop/src/app/source_symbols/tests.rs": { + "bytes": 85506, + "sha256": "aebd05f3fa9f1e9c2e744948d4478d9f267365d063f01f8b8f4e67560bc23f32" + }, + "crates/oa-desktop/src/app/source_symbols/tests/departure_selection.rs": { + "bytes": 11070, + "sha256": "81c8ab94a7dc388d5864bff2fe744df443b49a0915bc154fc8f77980440b0825" + }, + "crates/oa-desktop/src/app/state_attachment_escape_tests.rs": { + "bytes": 5729, + "sha256": "b2ddaa3a1f4e4beed2354505a297d58c201da18a07ad01109c3aea27270f166c" + }, + "crates/oa-desktop/src/app/state_bend_tests.rs": { + "bytes": 15830, + "sha256": "d3a95fa19ddb0ed60347bb682721efef63db6bb93aa2814f54d54a97267f6b3b" + }, + "crates/oa-desktop/src/app/state_diagram.rs": { + "bytes": 12806, + "sha256": "8f20722d2663a7ea7f54e8122a7fcade293caffc3e92c304b4a6bab033898ab8" + }, + "crates/oa-desktop/src/app/state_diagram_tests.rs": { + "bytes": 40068, + "sha256": "50187dbc48a8a5d9866db802e543c524698425fe694867c3cf5e951f6d53c788" + }, + "crates/oa-desktop/src/app/state_multiselect_focus_tests.rs": { + "bytes": 12969, + "sha256": "a094994cf77f698f69895226dc62d2aecf1be6f722fdfae2ae96194b9a015558" + }, + "crates/oa-desktop/src/app/state_multiselect_tests.rs": { + "bytes": 38490, + "sha256": "94504ddf9cfd8571c1df9f9119fb93242c2ca86ec024c2dc7ef059cf35243df8" + }, + "crates/oa-desktop/src/app/text_workspace.rs": { + "bytes": 24435, + "sha256": "cb8d351796893b047cb28e3f124afb2424370bc8890722e30429964c194bdfee" + }, + "crates/oa-desktop/src/app/text_workspace/lifecycle_tests.rs": { + "bytes": 23909, + "sha256": "077cb8592297ef04c506abf8a97773cee142078def0ea5428fb859356c7b7206" + }, + "crates/oa-desktop/src/app/text_workspace/tests.rs": { + "bytes": 34494, + "sha256": "ec35fe120e5bd55b9c575f084231669dc9749bdad7b4716bdb1e4e55d9a2be61" + }, + "crates/oa-desktop/src/app/transition_context_tests.rs": { + "bytes": 11508, + "sha256": "47002fb025f3a2f8c53871f9509bfe8611a61db6fb0e1b3125eb6e875425cb1d" + }, + "crates/oa-desktop/src/app/view_copy_tests.rs": { + "bytes": 7846, + "sha256": "296821c51dcde568e7d717039aa26bfc1b56a9074aa16051007bb351b33a4d44" + }, + "crates/oa-desktop/src/behavior.rs": { + "bytes": 133614, + "sha256": "be6a5f7537ec0f8c3c54767b829dd55c6fd7b5b64e36a4ed33294b42cf74ef02" + }, + "crates/oa-desktop/src/behavior/canvas_workspace.rs": { + "bytes": 7943, + "sha256": "cd2e4acf2cb504ed8135d9a7ae611fdf6de60c288b7ce47cf31b32ed22348326" + }, + "crates/oa-desktop/src/behavior/canvas_workspace/control.rs": { + "bytes": 7804, + "sha256": "2b8d97b8c0e3ff1b1a2c32187b1d0eb25edf39ba582b5fb3303bc880f674f7ba" + }, + "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs": { + "bytes": 8901, + "sha256": "33447003980847033e40550f9926d8617fa61e2c3e73ec9c6121d1afed946c33" + }, + "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs": { + "bytes": 27368, + "sha256": "de13f0112b1a39fa03f32a9308dcbd30b235eef9ead877b440dde023c93bc5a3" + }, + "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs": { + "bytes": 15164, + "sha256": "41ff9c50fa9fb89ab9fdb2d18dadc21585aafa1ee008cfcd1e0b731dce645eaf" + }, + "crates/oa-desktop/src/behavior/contextual_source_tests.rs": { + "bytes": 49518, + "sha256": "044bd412dc45ffe440b1e251993946942dc3759dcb2d5dfe71bd8d6ef61cbcc7" + }, + "crates/oa-desktop/src/behavior/creation.rs": { + "bytes": 28978, + "sha256": "33ae9aaf4ba90de9432870cc8e99708ef39e3638ae8283a9253c333785c91299" + }, + "crates/oa-desktop/src/behavior/creation/app_tests.rs": { + "bytes": 13181, + "sha256": "e9c5bf2b07357b7a2f679ae0d8f6904c1c1461aaeb94fa0db478b54d4641ae3e" + }, + "crates/oa-desktop/src/behavior/creation/tests.rs": { + "bytes": 22793, + "sha256": "a0d1daf9fb3920f7e6181a9802d84fbe056cbf0043b9e41dba5fdda687d9818a" + }, + "crates/oa-desktop/src/behavior/form_session.rs": { + "bytes": 3941, + "sha256": "f5027fb3a5a5599a182e7d1a10dba56dd66eb1f23a4ba4c4a1744c9a6a5b6e61" + }, + "crates/oa-desktop/src/behavior/scenario.rs": { + "bytes": 154718, + "sha256": "de3815b78858a1a271692f6b95983d8f070695bd34fcaa4443edd12bea37b075" + }, + "crates/oa-desktop/src/behavior/scenario/focus.rs": { + "bytes": 9728, + "sha256": "73a9ea10ad7312086081b8688396a21ab4251c9faecc2df6d984b702f3963533" + }, + "crates/oa-desktop/src/behavior/scenario/usage.rs": { + "bytes": 7643, + "sha256": "71744454700a800d58db17d66e9b531bd1bd97663b7330c17d94a33042c9f8a9" + }, + "crates/oa-desktop/src/behavior/scenario/usage/tests.rs": { + "bytes": 9306, + "sha256": "67d0e36d8de6d8a41191bbf82c7b708da330c9e0b6263c9bea285e976c0118c3" + }, + "crates/oa-desktop/src/behavior/scenario/workspace.rs": { + "bytes": 24960, + "sha256": "5aec4cb2ceb00ab25900d919a5bec71ab96848b4f14504f51790e3fe8801d3bc" + }, + "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs": { + "bytes": 80542, + "sha256": "f2f6acd93ecf4e9ffee56abc1de28f71da8cb1353cdc3e67cf0c0cf7a619fec5" + }, + "crates/oa-desktop/src/behavior/scenario_activation.rs": { + "bytes": 17694, + "sha256": "6dfc8eccf7245954319f2bec84072330a652bf208cc636c7043781d49dcd1ada" + }, + "crates/oa-desktop/src/behavior/scenario_activation/tests.rs": { + "bytes": 28464, + "sha256": "8b855dc067f1b15aed79c7eddf52c89001a8b961d9de4887f235c03b46264a1b" + }, + "crates/oa-desktop/src/behavior/scenario_lifecycle.rs": { + "bytes": 9348, + "sha256": "c94f3738e4bf027e3f055e32d709b90777d8fb2dea70b563713af8df000af13a" + }, + "crates/oa-desktop/src/behavior/scenario_lifecycle/render.rs": { + "bytes": 14769, + "sha256": "07703c54a14de636122bdb36ee9b23d3115152d3c548bd09827a838632e5dc07" + }, + "crates/oa-desktop/src/behavior/scenario_lifecycle/render_tests.rs": { + "bytes": 35358, + "sha256": "e0274c2aeb0463e0633f6cf7c4ce3b1a0f6f95b4084a1c1fb6cb6053288b2f1d" + }, + "crates/oa-desktop/src/behavior/scenario_lifecycle/tests.rs": { + "bytes": 15430, + "sha256": "01b576993b329810cd78a859b45ac52c66f5a14549065661dd1308196e0b724d" + }, + "crates/oa-desktop/src/behavior/scenario_lifecycle/union_tests.rs": { + "bytes": 18024, + "sha256": "1c7b6c40854d6206637ccdb1857962d5870d8df8fb44309aba43d250c405d2c8" + }, + "crates/oa-desktop/src/behavior/scenario_operation_tests.rs": { + "bytes": 10087, + "sha256": "89fb71c690e07eac314f7a5e05862d5c1e2f9c21039673bf22c4ac894d21c977" + }, + "crates/oa-desktop/src/behavior/source_draft.rs": { + "bytes": 18031, + "sha256": "08190823090bd8b0305809099abadcc0de431175f7044f1d5abdceda52612ee1" + }, + "crates/oa-desktop/src/behavior/state_bend_tests.rs": { + "bytes": 25480, + "sha256": "476d1493754b45477d7dfabd49e38568772d9263fd6880f263b5b10ef6744639" + }, + "crates/oa-desktop/src/behavior/state_canvas.rs": { + "bytes": 154375, + "sha256": "8503f7f1e216af3220bd69d06dab1b8e7a0910f0ef3992fca28be1de2ce71835" + }, + "crates/oa-desktop/src/behavior/state_edit.rs": { + "bytes": 45425, + "sha256": "5fcaa5d09ab6e6aa45f2cbdc430a9ce14a9afe6cdc7d72413114cee5706161c9" + }, + "crates/oa-desktop/src/behavior/state_menu.rs": { + "bytes": 12222, + "sha256": "87f2a534ef2651b8f527af832913894875f457d87a07d45df09c6cabe14783d4" + }, + "crates/oa-desktop/src/behavior/state_multiselect_canvas_tests.rs": { + "bytes": 14918, + "sha256": "58e0f8bc3c304c3ba51f6a8cc3ab002eaac05bf3d5bc115b0870aa191c9906fd" + }, + "crates/oa-desktop/src/behavior/state_view.rs": { + "bytes": 9535, + "sha256": "234ff0f4969c1e6067100795d1e75f5f6a7e8c22e374392becb83b23912f4f2d" + }, + "crates/oa-desktop/src/behavior/states.rs": { + "bytes": 26986, + "sha256": "8986ed9492960e20f1254e9dfe3ff91d79ce9fef66f3bda738f9933278043954" + }, + "crates/oa-desktop/src/behavior/topology.rs": { + "bytes": 87118, + "sha256": "2280fecc3ca3409b4847a3785b06b5bcbce34ec529cc0562e5c02653813e3dcc" + }, + "crates/oa-desktop/src/behavior/topology/contextual.rs": { + "bytes": 28432, + "sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a" + }, + "crates/oa-desktop/src/behavior/topology/contextual_routing.rs": { + "bytes": 16731, + "sha256": "d914b0e82aa8d253fe64be2ef74443d1fd2e6cec26d1cf30620a57646e1d426c" + }, + "crates/oa-desktop/src/behavior/topology/direct.rs": { + "bytes": 39059, + "sha256": "5cca649870f54abdd2eb098954bcc0a2a5ca456b7ee11b5f653edbdf2ed55905" + }, + "crates/oa-desktop/src/behavior/topology/direct_tests.rs": { + "bytes": 51170, + "sha256": "edccfe3ce8b224065a3540aa53c6340f13db0cecd48ecc0a3904cd832a483964" + }, + "crates/oa-desktop/src/behavior/topology/layout_tests.rs": { + "bytes": 37846, + "sha256": "f27203866e22aee80daad2811113341684b0cbd59885b36d64243c4317f29f84" + }, + "crates/oa-desktop/src/behavior/topology/usages.rs": { + "bytes": 9884, + "sha256": "6678741ad0d2e9d3691e1e209083dabf104eb2827f688e035714ed8f89e94458" + }, + "crates/oa-desktop/src/behavior/topology/usages/tests.rs": { + "bytes": 15482, + "sha256": "61aebacefc4c5f4d7326c59390823abe56b6ae74f6678be60b5889cca59e75bd" + }, + "crates/oa-desktop/src/behavior/topology/view.rs": { + "bytes": 22882, + "sha256": "df183ce35b9186ed2083385f3c1a01109a8f41783c7e7a7988e117e34f2f1585" + }, + "crates/oa-desktop/src/connection_edit.rs": { + "bytes": 5301, + "sha256": "020581e077f82b43c936b623cc51733b02b88428df4e5f25df74b9705819fa37" + }, + "crates/oa-desktop/src/connection_tool.rs": { + "bytes": 27023, + "sha256": "649da93677d52dae495b0e19bc32f3122ab05a6d84de8402578ff5ce3f6ee1d3" + }, + "crates/oa-desktop/src/context_menu.rs": { + "bytes": 59453, + "sha256": "03990ad926c6221d091b07e496b0614f264c9785f1489896be16e3711570ca55" + }, + "crates/oa-desktop/src/data_values.rs": { + "bytes": 23877, + "sha256": "e1deb1cad551f0a9fdef9653aad257cfb36e6d9c0f93b6245061ec3fab8fd365" + }, + "crates/oa-desktop/src/data_view.rs": { + "bytes": 38871, + "sha256": "158b852be5d3db450aa5cda8b68a35bd96bddce1d8d053a7a23582095c06362d" + }, + "crates/oa-desktop/src/diagnostic_input.rs": { + "bytes": 10225, + "sha256": "7083fbd65cf0b55ef0a71979eadaea8b298fe5770b9b0738fb8aa1edc06d0a98" + }, + "crates/oa-desktop/src/diagnostic_input/tests.rs": { + "bytes": 20485, + "sha256": "1b1544054a214625df67049bd6a321575fcea220f03d70b75c7b5fde635676db" + }, + "crates/oa-desktop/src/diagram.rs": { + "bytes": 120821, + "sha256": "d7971317f2c22ca0c3a424cd9f9d7873a367dd3cb9bcc8f19a379c7b637dbd8d" + }, + "crates/oa-desktop/src/diagram/attachment_tests.rs": { + "bytes": 14726, + "sha256": "f1ac82893330983075069a9161e98505604e406bbdacd5d594b5f7f9e9a2a2d6" + }, + "crates/oa-desktop/src/diagram/focus.rs": { + "bytes": 10722, + "sha256": "a5c37af824aa5cebaad5984bc61e3347e4707f38b9441633014fd543c67e3965" + }, + "crates/oa-desktop/src/diagram/group_gesture.rs": { + "bytes": 21633, + "sha256": "31c5fc34ebc22a50b9794718da01f4ce016ec29757be1b5473b21f746462f8e9" + }, + "crates/oa-desktop/src/diagram/lifecycle_tests.rs": { + "bytes": 9434, + "sha256": "968f0e1c39e00432fc4bb255fb9dece2093b1d5280a558814f1c31e4022fb4e2" + }, + "crates/oa-desktop/src/diagram/multiselect.rs": { + "bytes": 10989, + "sha256": "c8adf20046176f620caef52dd8ed93eedb349f72d8c9f50401aaa440848803b4" + }, + "crates/oa-desktop/src/diagram/multiselect_tests.rs": { + "bytes": 22746, + "sha256": "ef2d2b1719e3bb9aa378e0370af9cebcbe2b79e088cedf562b911c13f5d4135a" + }, + "crates/oa-desktop/src/diagram/pointer_tests.rs": { + "bytes": 11951, + "sha256": "a68efa6ccd59faa78c12df2321a2169eea952b1f039ec2164af6705456f82a65" + }, + "crates/oa-desktop/src/diagram/port_hit_tests.rs": { + "bytes": 5742, + "sha256": "dde66b3db9a0c86ba6f8e0604b8da7a84becdcd4dae871cf3a13c004fb9f3b95" + }, + "crates/oa-desktop/src/diagram/port_labels.rs": { + "bytes": 13678, + "sha256": "f66b5ecd2dc64174324b34b4b5ebeb9c303ead25ebbb43304cecff0336cf3d15" + }, + "crates/oa-desktop/src/diagram/view_options_tests.rs": { + "bytes": 2815, + "sha256": "cf0c98a7715558baf17b59f67a80556f98ad30262b4434635e617c6601775433" + }, + "crates/oa-desktop/src/diagram/work_drag.rs": { + "bytes": 7059, + "sha256": "9757534a7ba47dc0df5d41c8d7366b1d90f3d50b24344580ff3c783667bf66f0" + }, + "crates/oa-desktop/src/diagram_selection.rs": { + "bytes": 14219, + "sha256": "a105f2877f2cb5d0bbcec6fbbd770178347895ee3a2b281a1ed93ad4b1c7d4b8" + }, + "crates/oa-desktop/src/diagram_usage.rs": { + "bytes": 15424, + "sha256": "e4eb0ff476b89a4aeb025eb538070745e29411c5eb6841f6350a73246eb323ce" + }, + "crates/oa-desktop/src/diagram_usage/coverage_tests.rs": { + "bytes": 13503, + "sha256": "7b665bafd98f13cc2c0e3f608763929a732da0bfb60f3287b66cd5be9c69adc3" + }, + "crates/oa-desktop/src/diagram_usage/tests.rs": { + "bytes": 6517, + "sha256": "e35c8a60a566a33fee829161b77fb60ab0ee36e258bc78a2833725a152b4fa26" + }, + "crates/oa-desktop/src/diagram_usage/view.rs": { + "bytes": 7230, + "sha256": "4cfd71f476dcf4e4ede36937e32b652b634e87fbf91c3fe02bfe5b2f1996544e" + }, + "crates/oa-desktop/src/document.rs": { + "bytes": 32600, + "sha256": "e87d7ce456bcab1dab55249021eaa8056f41b888723d143428486e31cb9ca4b1" + }, + "crates/oa-desktop/src/document_alias_rename.rs": { + "bytes": 8021, + "sha256": "ce64d723a51a834699ebcf74d89d406a77b6ccc5419bcd3f63cf0e27e9fd25cb" + }, + "crates/oa-desktop/src/document_alias_rename_tests.rs": { + "bytes": 18027, + "sha256": "847dca1e50970d8a3d75c7e881754453e801f4f3d34f03a88410a334899783bc" + }, + "crates/oa-desktop/src/document_save_as_tests.rs": { + "bytes": 5781, + "sha256": "83dc243a25f4e97674ff0d86c794a490cc923e98f1f9fb05bb038f622f11e20b" + }, + "crates/oa-desktop/src/explorer.rs": { + "bytes": 14764, + "sha256": "fa01c59219564ba9ae5553abc2b6f94e9849f9148d5812ef99b40318fe5dcf9a" + }, + "crates/oa-desktop/src/explorer_view.rs": { + "bytes": 13773, + "sha256": "a720d7484bcc3a24729332a00e005d1cc6f5f945a9cc3965134e0024dffb581f" + }, + "crates/oa-desktop/src/forms.rs": { + "bytes": 105544, + "sha256": "ea124f479fcd2d3978ac3cb8eb81fec4a7aae5e4f4414c4bdd54849e954dedf9" + }, + "crates/oa-desktop/src/forms/context_fields.rs": { + "bytes": 10831, + "sha256": "eaacd474f6500dd344d5a8b63ccb1d1dd86fe2aea78ab708afa8a80d62ce69bf" + }, + "crates/oa-desktop/src/forms/data_fields.rs": { + "bytes": 49285, + "sha256": "a540a0d451bff56a96b1d28643b2e8c0649f3846350c25c5b154729c0538e77c" + }, + "crates/oa-desktop/src/forms/history_fields.rs": { + "bytes": 27414, + "sha256": "e7100474cde18bdfe95562bcb3c055f29b4b15aa64788a578194464aebcd046e" + }, + "crates/oa-desktop/src/forms/history_fields/tests.rs": { + "bytes": 33950, + "sha256": "16d3c308138c063f45986d53c621ad83c7dc2ce853edd31b7204e2939a382df9" + }, + "crates/oa-desktop/src/forms/port_fields.rs": { + "bytes": 30554, + "sha256": "3fe684d651d5be6203fd9db3979c772913e1af5ac1514e67be386ffd88dc8127" + }, + "crates/oa-desktop/src/forms/reference_chooser.rs": { + "bytes": 12130, + "sha256": "b60af363be333af048859e51c28e8c989cb5c95d696cad16c8eec15f78d57a12" + }, + "crates/oa-desktop/src/forms/reference_chooser/adapter.rs": { + "bytes": 10447, + "sha256": "87ab4bc3bf27fd4b5a458791cf62c7ba8e44e9ebf846d8b851b8b6ffb454cd64" + }, + "crates/oa-desktop/src/forms/reference_chooser/tests.rs": { + "bytes": 27835, + "sha256": "8e98f8a3fcb814d4a201cc85b3db87a689fd3d2ce487f8a2fe312fc296f93377" + }, + "crates/oa-desktop/src/forms/reference_chooser/trigger.rs": { + "bytes": 13340, + "sha256": "5c180c18775186eae997814e9c0050813edb202c535cee665e046d40aadb52d7" + }, + "crates/oa-desktop/src/forms/reference_chooser/trigger/focus.rs": { + "bytes": 9923, + "sha256": "f1e79f4b03b2f9fd1cdc335054d4ae177dc9e759b6ea3d334b2bcca39b57191b" + }, + "crates/oa-desktop/src/forms/reference_chooser/widget.rs": { + "bytes": 41150, + "sha256": "57f21b2e438e7fc89a0a33412f8e43d9b3f7e59042c48a025bbbebe3d59467c8" + }, + "crates/oa-desktop/src/forms/view_fields.rs": { + "bytes": 24987, + "sha256": "5184d438f0dfd4e29f284ff048e19da73e71be1f24a6e9f123c7e922aecb85fa" + }, + "crates/oa-desktop/src/guide.rs": { + "bytes": 123554, + "sha256": "954a7ac9ac9b57443ea7e98cadb37a1ee42c9103b48c8ccb4e933ed433ad8d75" + }, + "crates/oa-desktop/src/highlight.rs": { + "bytes": 2736, + "sha256": "8222e2deed5d60ea522241ccb2c653caf50ecc1d4404d633f54e0193c5f622e9" + }, + "crates/oa-desktop/src/home.rs": { + "bytes": 17587, + "sha256": "f03c1071334894f586a5a7c86026602a694db3c2faf902822e1dbe5156a859c2" + }, + "crates/oa-desktop/src/home/tests.rs": { + "bytes": 8167, + "sha256": "3b03ccfc97dfb75954e1e0ed568bb87a632295c07b96cedc05ed381f87b378f3" + }, + "crates/oa-desktop/src/hosted/client.rs": { + "bytes": 21912, + "sha256": "54b24b4b50f8d87d7c92946ea5a2cff4c98760e70555b3ce3d90be27115e95d4" + }, + "crates/oa-desktop/src/hosted/client_tests.rs": { + "bytes": 23009, + "sha256": "3ef70907ca75e098d631c41b90badd4cff1e1eb1b3c78e15a9f811c13ba8ffbe" + }, + "crates/oa-desktop/src/hosted/mod.rs": { + "bytes": 3744, + "sha256": "1b75e11c80fcb38fd20bb3487e3f88729fdac73e471c5ae64da28c249f77c63b" + }, + "crates/oa-desktop/src/hosted/view.rs": { + "bytes": 9417, + "sha256": "862180b048e6ae28fa188aa889ab5b584061604c7cd7670ab83ec4dbf8c9ae91" + }, + "crates/oa-desktop/src/main.rs": { + "bytes": 1661, + "sha256": "875b6385225ea63e8809a0965950fa49061332572dfa83511ed83b970e09f290" + }, + "crates/oa-desktop/src/model_menu.rs": { + "bytes": 44572, + "sha256": "3844be8ec15fe2c5614ff5625ec7b63234fbf5a6d126de5a85c8f5c51dfa7101" + }, + "crates/oa-desktop/src/navigation.rs": { + "bytes": 7527, + "sha256": "7b2bc56ea9ea1ecbe14536d905b6e18e8e72d11c0c31024e625187886bf3bb65" + }, + "crates/oa-desktop/src/port_edit.rs": { + "bytes": 7809, + "sha256": "70b99f577c4fdeda8780c8dc1ee7b1639fd77aaca06658f06d725139fb3efb6d" + }, + "crates/oa-desktop/src/port_preview.rs": { + "bytes": 35855, + "sha256": "069487c4ece737d52e8ae4a84dcfb132236aca6799e841c91c8e7244320cf871" + }, + "crates/oa-desktop/src/port_workflow.rs": { + "bytes": 40582, + "sha256": "ccc15f97c3272145a67bb5857b4faeaf7437561bd61912fa27ab26e8824146a0" + }, + "crates/oa-desktop/src/recovery.rs": { + "bytes": 21041, + "sha256": "57e22e5fbb0e994e64ae03e69450271fe9f04a951db1670c2a018abe99db4489" + }, + "crates/oa-desktop/src/recovery/fixtures/README.md": { + "bytes": 1587, + "sha256": "a90002df1e087d2dac7be26926864243568a0de7c4103e4927435b34178e8016" + }, + "crates/oa-desktop/src/recovery/fixtures/profiles.json": { + "bytes": 473, + "sha256": "2c12ae96b50e6da5d60ce2b7fa00204be71dfbe2fd51311fc337a0de6a4332ff" + }, + "crates/oa-desktop/src/recovery/fixtures/source5-bundle.json": { + "bytes": 80974, + "sha256": "12715995e6f7f35414aea53b3ad720b9b31873ffdeaf4444231db1a5ac3c463e" + }, + "crates/oa-desktop/src/recovery/fixtures/source5-model.json": { + "bytes": 86191, + "sha256": "5535a706df9ec21715e8b6a5aa6dbd79636ae461450aee61305559016167612b" + }, + "crates/oa-desktop/src/recovery/fixtures/source6-bundle.json": { + "bytes": 85286, + "sha256": "ea88890a33f47d67dfb06d6dbbcc179058cf28dad76ae7d615c00aaf30dbbbfe" + }, + "crates/oa-desktop/src/recovery/fixtures/source6-model.json": { + "bytes": 90629, + "sha256": "5bb8db0b6394276666323483f977ac3586f88554c81a148c6d508fca0ef4576f" + }, + "crates/oa-desktop/src/recovery/source5.rs": { + "bytes": 6956, + "sha256": "0f0a763f0c98c2c908d4c2b95d8a4c042b6a255ee1cc16260e5404669691f642" + }, + "crates/oa-desktop/src/recovery/source5_tests.rs": { + "bytes": 12146, + "sha256": "cfd17a742f1f27b523fce7cd9309f0a07a30413395f6c58659fb91d02834a7a3" + }, + "crates/oa-desktop/src/recovery/source7_tests.rs": { + "bytes": 18213, + "sha256": "7ee61de66c75bf949489ba51ddc9677e2b4efc68801cf27909c9b894bfdc9288" + }, + "crates/oa-desktop/src/route_edit.rs": { + "bytes": 5607, + "sha256": "cb39d188c283eb80d24f145efbd8fd3bf3582c90c96c6b249c3233e9c107af91" + }, + "crates/oa-desktop/src/semantic_review.rs": { + "bytes": 3432, + "sha256": "8dd1e93783d63a663c7e56c28775a5c389cfd7a6b562013d6a95c891afa21ed8" + }, + "crates/oa-desktop/src/semantic_review/artifacts.rs": { + "bytes": 24201, + "sha256": "a79236a8150597ff808597e59cf4f1e9e61c74c4901d9c8cf42d3427bb52a9a8" + }, + "crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs": { + "bytes": 11034, + "sha256": "3f255d04e4bffa5a467b75e3cbf6f7149aa3269a02f2a89fbaa1db0e5df9ae62" + }, + "crates/oa-desktop/src/semantic_review/artifacts_tests.rs": { + "bytes": 20798, + "sha256": "04d0dd2ac453705185663f705282aa4a3a31433466a5be305764248da1a03c32" + }, + "crates/oa-desktop/src/semantic_review/artifacts_view.rs": { + "bytes": 6447, + "sha256": "2100ff73565765d8293c84f6ce2d8e566610c1524c50e32ce3c37dc33b6686e4" + }, + "crates/oa-desktop/src/semantic_review/attachments.rs": { + "bytes": 18160, + "sha256": "de0ec0c740895b160ff5b847332c087c3f1a48931583464069470cc61315ed59" + }, + "crates/oa-desktop/src/semantic_review/attachments_tests.rs": { + "bytes": 29389, + "sha256": "8b26be0324e0f33b1e7cf7af340350df8ed98963002d6da8589ee0e6f6900bea" + }, + "crates/oa-desktop/src/semantic_review/attachments_view.rs": { + "bytes": 3032, + "sha256": "932f231554d77c7cb119d33bb06b9879db5e12127aed187e67013e77b6458e78" + }, + "crates/oa-desktop/src/semantic_review/batch.rs": { + "bytes": 34485, + "sha256": "a462eb627ae19348072775cf8e60b17d62f6b682b74cc413906d493fba1f0b3c" + }, + "crates/oa-desktop/src/semantic_review/batch_tests.rs": { + "bytes": 27546, + "sha256": "b67c4d7dcabac710cf3d30730a8db323180a558a415e2d117c868cdfa4804619" + }, + "crates/oa-desktop/src/semantic_review/compose.rs": { + "bytes": 27155, + "sha256": "c6adcc9882dde65e9f04f6eda7f0c855c5cbb285cf421123029169112793ce9b" + }, + "crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs": { + "bytes": 9809, + "sha256": "440daf8d43278ef981c620d1818d0daaed8fb83193b8e8ff67b2e42fbf882938" + }, + "crates/oa-desktop/src/semantic_review/constructor_tests.rs": { + "bytes": 54132, + "sha256": "85149521ad7df62918f6f2fab6da2b3346c342ab4bce7100de3f6206c1e81096" + }, + "crates/oa-desktop/src/semantic_review/constructors.rs": { + "bytes": 12258, + "sha256": "0982163439b06a42d9af1643530364109282e63f05b323a35a76d8241f3caa32" + }, + "crates/oa-desktop/src/semantic_review/history.rs": { + "bytes": 3211, + "sha256": "df038c2464eab8093a983f3104f0d04ca8f602010a50e1d3eeb8e53f2309e201" + }, + "crates/oa-desktop/src/semantic_review/keyed.rs": { + "bytes": 14175, + "sha256": "bdd6c0fdc7108056bc5d7433254503449eed2209813680e717ead24b9d0ce834" + }, + "crates/oa-desktop/src/semantic_review/keyed_rows.rs": { + "bytes": 13222, + "sha256": "4b7bf25806471ad22d89253b9af8000f5acff53b4cc0aa1c1a83138e7b270eca" + }, + "crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs": { + "bytes": 30804, + "sha256": "1a3b09e7ed100411cdf8043f9a78defc664a873ee0201117709185c201a5c57f" + }, + "crates/oa-desktop/src/semantic_review/keyed_tests.rs": { + "bytes": 31163, + "sha256": "9dc9b989e5a9d408427ca1baf86b2454f9dd3baf40a551c70ca2359dc18a1227" + }, + "crates/oa-desktop/src/semantic_review/message_kind.rs": { + "bytes": 789, + "sha256": "4ed19952ea511d1b3226564a138e3322f994371d6b49794c14bc3689d1393e7f" + }, + "crates/oa-desktop/src/semantic_review/message_kind_tests.rs": { + "bytes": 23424, + "sha256": "5fcf9930e949aebeb12eab0327264f32a2388d62dbf098bd2365711e22623c52" + }, + "crates/oa-desktop/src/semantic_review/ordered.rs": { + "bytes": 9705, + "sha256": "6a1ed894d1b9d0110bc784a75c49444f1d14f1f74620d701f22203b2d8d0a813" + }, + "crates/oa-desktop/src/semantic_review/ordered_tests.rs": { + "bytes": 22383, + "sha256": "5c8ea3a69e3da3b94d13f50f12aa7dc53246ccba25ffcace9552a0eb76b0bf36" + }, + "crates/oa-desktop/src/semantic_review/recovery.rs": { + "bytes": 28769, + "sha256": "88e69044d90c481593bab7cc125f0401c05202fccdde07698cec9483947fe8be" + }, + "crates/oa-desktop/src/semantic_review/row.rs": { + "bytes": 38159, + "sha256": "b08fdc503c6c4d2ff99adac7e18c9baa861bf86155e00b55d1e492430980d0f2" + }, + "crates/oa-desktop/src/semantic_review/scalar.rs": { + "bytes": 15153, + "sha256": "9ad202174934dd9a63fbf9cba43135aead571db60846c8b7b1d3019a11df26c2" + }, + "crates/oa-desktop/src/semantic_review/source7_tests.rs": { + "bytes": 11665, + "sha256": "5e8221d77b068fa30c1571988448cb8829711735881d810f3d333cbc95d34a9f" + }, + "crates/oa-desktop/src/semantic_review/tests.rs": { + "bytes": 32480, + "sha256": "077ec6501dfaab0b05ea3987943b204f08352952296d313555051d998ccd232e" + }, + "crates/oa-desktop/src/semantic_review/view.rs": { + "bytes": 57626, + "sha256": "7cc7dd213ab01dadad5debaee868c30a1099466602869a799ebdf9b680e8eaf0" + }, + "crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs": { + "bytes": 4737, + "sha256": "6c766b29c8664d15cb113d2cc71bba9b60ae4ccf7e44c9c5d3c41e1dc4b7ed90" + }, + "crates/oa-desktop/src/source_annotations.rs": { + "bytes": 12084, + "sha256": "517fc93890f10196b5501e8728dbe574a9c4bff5847e43a434060933d6e55465" + }, + "crates/oa-desktop/src/source_annotations/anchor_tests.rs": { + "bytes": 10026, + "sha256": "52580dcc70dddbee9cf96e2e1ef2558183593ea7c45c155e576925d9bfcd2b5d" + }, + "crates/oa-desktop/src/source_annotations/layout_tests.rs": { + "bytes": 11345, + "sha256": "234c72d925f96de43e41c9231b01e2b80f68ffc3519d052bac5f248092f9ff50" + }, + "crates/oa-desktop/src/source_annotations/performance_tests.rs": { + "bytes": 7841, + "sha256": "7d97a449347938067e1d5d70b285f49dab01f0e29f7e2e378319a8f020216879" + }, + "crates/oa-desktop/src/source_annotations/tests.rs": { + "bytes": 2389, + "sha256": "3de4f79fa2f31e9d94fd6dc97ccdb33fee81d1b5bb7bcf9770ea1af3513bd29b" + }, + "crates/oa-desktop/src/source_annotations/widget.rs": { + "bytes": 13731, + "sha256": "f8043aa33e626c71e5945dc3fdd2533cbb4aac504686e123794179a5ce905771" + }, + "crates/oa-desktop/src/source_annotations/widget/tests.rs": { + "bytes": 38443, + "sha256": "9b2f3b10df672866dbe0ecfac693ebf3eaa7d6172dbbfce36af44946b1d9020a" + }, + "crates/oa-desktop/src/source_navigation.rs": { + "bytes": 4049, + "sha256": "3e5a1753140fc50e4d3833bf6fcf572b8d387aa1c8a8af85d3b5fcde7026ffbc" + }, + "crates/oa-desktop/src/source_navigation/controller.rs": { + "bytes": 3680, + "sha256": "938b8264c2d5166bd979439723d6fbec34c4cc5ed7f872f58c2cb9ba06a4a93d" + }, + "crates/oa-desktop/src/source_navigation/interaction.rs": { + "bytes": 8808, + "sha256": "c6efa6105b8ba46655e27a89b9fa59e6fc01a4c6f331f886e035af1e70cbf8a9" + }, + "crates/oa-desktop/src/source_navigation/interaction/tests.rs": { + "bytes": 6616, + "sha256": "6cf5c5944b9c46c275c9039f7166b627fa9176187b39348152f67040c92c95e1" + }, + "crates/oa-desktop/src/source_navigation/query.rs": { + "bytes": 36149, + "sha256": "4f6ff7d708bb99c64b7370a1761a6099d5f97be4b319582ab4faa7bb8aa42bb9" + }, + "crates/oa-desktop/src/source_navigation/query_tests.rs": { + "bytes": 31577, + "sha256": "121f197dbae319f47ec59ff875733f1985c7607badb2e4c3d97ef8746d13afa9" + }, + "crates/oa-desktop/src/source_navigation/selection.rs": { + "bytes": 11322, + "sha256": "b0279456fafaca54880b61b970839a55d8affde6c6f9e24d86cb99bb41c388d6" + }, + "crates/oa-desktop/src/source_navigation/symbols.rs": { + "bytes": 68677, + "sha256": "01296588309f2273d83659b9dd5c132878423dd0c94389cfe9f35a1e575d77da" + }, + "crates/oa-desktop/src/source_rename.rs": { + "bytes": 116, + "sha256": "1ead2a66d9fd7429b8cfca1338b038e7dd97e21ae76cb722f3ca4c43a520ca00" + }, + "crates/oa-desktop/src/source_rename/origin.rs": { + "bytes": 9275, + "sha256": "11f619310fe837f20a8d495b13e557cafe258270350815c6ac8c848f51cd76ac" + }, + "crates/oa-desktop/src/source_rename/preview.rs": { + "bytes": 19903, + "sha256": "bf96e31986d143fce43ad1634d7028f1225aebcd9013955ca2ba18cc4fd1190d" + }, + "crates/oa-desktop/src/source_tree.rs": { + "bytes": 21816, + "sha256": "4d55e0110a1eb128a5d26c28f3aae5853d79b7245812b7c82bd275f38440a5aa" + }, + "crates/oa-desktop/src/source_workspace.rs": { + "bytes": 24090, + "sha256": "388c71f63829c0c76526a1700ebf5f240c86c467a65aae4fca4cd6c6a88c468b" + }, + "crates/oa-desktop/src/style.rs": { + "bytes": 6175, + "sha256": "28b9527137d03850b617e3c248828e8111743e3b2d0ca8def31d8f381889ddd2" + }, + "crates/oa-desktop/src/style/tests.rs": { + "bytes": 13447, + "sha256": "b771d9656beafc1489ecef5e97d9b006ac385f77933930c42769cc35ca8ddffe" + }, + "crates/oa-desktop/src/style/tests/readonly.rs": { + "bytes": 7905, + "sha256": "89ec0fc1678e4673bf1495bece8c0d3b1a12260b2246f33f92029947c3afd2c1" + }, + "crates/oa-desktop/src/tests.rs": { + "bytes": 59726, + "sha256": "0552b7557b223bdf114ed37429940dc84e4c2830dd6ca30ca26744b29762311b" + }, + "crates/oa-desktop/src/view_copy.rs": { + "bytes": 9117, + "sha256": "4d2ef73b02c5e09692838325ec2b9abff07f2d600cfbeb9c2b612fe03cda10b4" + }, + "crates/oa-desktop/src/views.rs": { + "bytes": 60842, + "sha256": "c3a394354a04206853cf2fbf0d7c7923874a254f7480d2d27ac35294f1ceb503" + }, + "crates/oa-desktop/src/views/source_symbols.rs": { + "bytes": 13267, + "sha256": "9956b999c5a98703eda704f84f848c545abceb8f1fa37c6115bf42726f851796" + }, + "crates/oa-desktop/src/views/text_workspace.rs": { + "bytes": 44192, + "sha256": "389aed598746c159bdaf9e7a6737047e549a98dcfeca04b8ebb2af4b0a288065" + }, + "crates/oa-desktop/src/views/text_workspace/summary_visibility_tests.rs": { + "bytes": 9819, + "sha256": "486cccb022402e39babda703781fd3d98d575e1eda8b12356105a01119a65179" + }, + "crates/oa-desktop/src/views/traceability.rs": { + "bytes": 7501, + "sha256": "4c89dcf35c25e737d8cce4d543a5989ca104a5678e6cf40df47e457773d2ef94" + }, + "crates/oa-desktop/src/workflow.rs": { + "bytes": 82963, + "sha256": "d9e7b11a86fd3ec79d91f0c9bc9cebd5a2043cdaa7261622454fb7730ad8e060" + }, + "crates/oa-desktop/src/workflow/communication.rs": { + "bytes": 11084, + "sha256": "34d121ad97706012722e4bb6210072ecc3a5b4718b0ceab0c3d0f14d4cd780a1" + }, + "crates/oa-desktop/src/workflow/recovery.rs": { + "bytes": 9859, + "sha256": "0849b0f34dba5e8761be5fa05784d134ac74e4e9db395d6b9b7d6cff71e3f3dc" + }, + "crates/oa-desktop/src/workflow/transition_tests.rs": { + "bytes": 8627, + "sha256": "92c9b1a81aec3b8f10845819a439b0fd88e86b178b6396a1cb7bc0cbc4b52dba" + }, + "crates/oa-server/Cargo.toml": { + "bytes": 778, + "sha256": "527ff5e15010a8b560a6659aa1ae4bf0c8df0d9e019f03c47529ec6a750c0982" + }, + "crates/oa-server/build.rs": { + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + "crates/oa-server/src/error.rs": { + "bytes": 5610, + "sha256": "93817f1502386e4662c96ee1c7d1c4afc4e33bda6fd64e46c86c8e1afd5a9368" + }, + "crates/oa-server/src/lib.rs": { + "bytes": 7743, + "sha256": "20bef350350ed6339588b23af684abf745f97916c7edf3853ee042abe687e664" + }, + "crates/oa-server/src/main.rs": { + "bytes": 4163, + "sha256": "d59a6a2b5c7034bd2e00a516d2d49abb16391bf0316c1c357ddafb9c82d2d1e3" + }, + "crates/oa-server/src/preparation_http_tests.rs": { + "bytes": 5523, + "sha256": "454b90d0055d35644c8d990cd7576ddf675a9b271ca07fe9b0efce25d8ddbc07" + }, + "crates/oa-server/src/routes.rs": { + "bytes": 46053, + "sha256": "3f9a0c22043dbf28f7a8e587d2fc3abf869f76f4d23a46fe0aeb9c4392454a4b" + }, + "crates/oa-server/src/store.rs": { + "bytes": 25524, + "sha256": "37dcfb845b7993d5bced2bcf25fedfcd2a9c20a748f9315afe6cca1f41f89ae5" + }, + "crates/oa-server/src/store_preparation_gate.rs": { + "bytes": 4440, + "sha256": "427c390da3c0281ba1fe115a93ea90800e3b45fce6bcb9185e71982ddd885fec" + }, + "crates/oa-server/tests/http.rs": { + "bytes": 71951, + "sha256": "2b225b047a7fc020dddecabe3345f7742a0aa0b00a27e00c383d28ea90e9117d" + }, + "crates/oa-server/tests/http/communication_limits.rs": { + "bytes": 5790, + "sha256": "ba5474dd8e494a0176fffb4548c8e3deb2081b15de33bfce82394c764e0a2c16" + }, + "crates/oa-server/tests/http/data_contracts.rs": { + "bytes": 25949, + "sha256": "8cd456db83f301baca380acf025a5a99db4edf7d8839537dfbd02c4307e10e6b" + }, + "crates/oa-server/tests/http/edge_attachments.rs": { + "bytes": 12840, + "sha256": "1220a1c4b5818e6796d476c938c3e67316e27df18872a5272c9974ab1381ee7b" + }, + "crates/oa-server/tests/http/port_anchors.rs": { + "bytes": 20862, + "sha256": "a00d59a9fc78850c39e6207b2706cfa1ee95b3bcf205f7104efb33c7cd5bc7c8" + }, + "crates/oa-server/tests/http/semantic_transitions.rs": { + "bytes": 22832, + "sha256": "856d1c9b633ffb581e3e631f4e5a607789a02181dbf060667f208a569bfa4920" + }, + "crates/oa-server/tests/http/source6_views.rs": { + "bytes": 13023, + "sha256": "867bccda0e030eeeb9976b4ceeb47b21ab5044d700278022d5fb495bbb64ca07" + }, + "crates/oa-server/tests/http/source7_contracts.rs": { + "bytes": 11000, + "sha256": "0e0a467186da7263c0754890aff69ebd0d92d36b5224e4865bb6854b18dab7e8" + }, + "crates/oa-server/tests/http/source_projects.rs": { + "bytes": 30907, + "sha256": "8ff580f317ec267d1cd72ae14d8638f74a77cdf99ca4c78f66f4290c2d77460a" + }, + "crates/oa-server/tests/semantic_http_contract.py": { + "bytes": 7251, + "sha256": "4737ff94f10761f94151a694414c5c62737e0391ec820bca132830652bc4cca2" + }, + "crates/oa-server/tests/semantic_schema.py": { + "bytes": 25111, + "sha256": "cacca116c42ffbc097f1419c0f89921fa0dfd3faa864d46820ff744cfe6967f9" + }, + "crates/oa-server/tests/state_representation.rs": { + "bytes": 12241, + "sha256": "6686c826f28248ae4b63ec3f2112e193d4a347724b87974483217fcfee49cd61" + }, + "crates/vendor/iced_widget/.cargo_vcs_info.json": { + "bytes": 100, + "sha256": "58c111ee6197295ae381d021ab7afd7ac38b02acce90d0ff0f82ae2e1e5f864c" + }, + "crates/vendor/iced_widget/ARCADIA-PATCH.md": { + "bytes": 1885, + "sha256": "31fff95a2a5acfed5cb6cd1870fce65df29be1c19bbf9460f8e25a9b4955edfb" + }, + "crates/vendor/iced_widget/Cargo.toml": { + "bytes": 2622, + "sha256": "e8e6b9a72fc0f97c84b8fb2703fe6cdccd738c6c01d2fadf018a012182de93b5" + }, + "crates/vendor/iced_widget/Cargo.toml.orig": { + "bytes": 1109, + "sha256": "f9bed37f4aa1edebea47824c1f9678411d410a1546401a14ce6f8b4108c96448" + }, + "crates/vendor/iced_widget/LICENSE": { + "bytes": 1073, + "sha256": "fc9086ba4eba4b77e4c603906362742bc3715ffa2c7810c6074fe7780d3a0398" + }, + "crates/vendor/iced_widget/UPSTREAM-MANIFEST.json": { + "bytes": 10283, + "sha256": "1be311c5d9c328dd986231deac742ceff1d53a309aa06569f8928546770a7393" + }, + "crates/vendor/iced_widget/assets/iced-logo.svg": { + "bytes": 1892, + "sha256": "8ac9e30de1b466b3d594c77c984299b1a229f6f0f010e6804139a2f41ea8345e" + }, + "crates/vendor/iced_widget/src/action.rs": { + "bytes": 2507, + "sha256": "feb91a386073280d76eaf37dbb8e7380e5a6bf9b8003110107b32bb8f577a239" + }, + "crates/vendor/iced_widget/src/button.rs": { + "bytes": 21150, + "sha256": "a8019fee3ad691fb2395afcdf302bec5f9e1cc592552aff4d8c8bbd1c6149f5a" + }, + "crates/vendor/iced_widget/src/canvas.rs": { + "bytes": 9637, + "sha256": "1fecfd50d593c5ff9bc9317c9f1624d0c22eddc7cf39a795684bf70eb5954acd" + }, + "crates/vendor/iced_widget/src/canvas/program.rs": { + "bytes": 3099, + "sha256": "637bbc6d10a623f355c6d44af1162a10331861806cec094e2a8f2a97313acab9" + }, + "crates/vendor/iced_widget/src/checkbox.rs": { + "bytes": 20973, + "sha256": "4409c11d6473baf4baefafbb3ed253cd9353f04136cac6009422a876a84ec6b1" + }, + "crates/vendor/iced_widget/src/column.rs": { + "bytes": 17490, + "sha256": "f880d15ebdb33e2f6ebb6067bb0b124c8fafdc73a6e5112a6f5b5e012fddadf4" + }, + "crates/vendor/iced_widget/src/combo_box.rs": { + "bytes": 30694, + "sha256": "973901d05003f684be1d13da5adc88ef7e030dc31db07c1bb0717cf9210d2391" + }, + "crates/vendor/iced_widget/src/container.rs": { + "bytes": 18126, + "sha256": "f8162a4e1c25c602070da482ecccfd5e62f456ffe1caa5394a11243d339f6114" + }, + "crates/vendor/iced_widget/src/float.rs": { + "bytes": 12092, + "sha256": "9bc2d313052c97338dcdec2a5be7e0301b6261fe7975b511050fe4abeeb1c571" + }, + "crates/vendor/iced_widget/src/grid.rs": { + "bytes": 11862, + "sha256": "01e4deea4638fba19e3859d7ae27bd8090bca3fcee6f31c48bf5ad9a19f5c151" + }, + "crates/vendor/iced_widget/src/helpers.rs": { + "bytes": 60874, + "sha256": "3989cad7e901ff8171a16024d93ec40cb32e7859c2e76a98359f5f230a2fabcb" + }, + "crates/vendor/iced_widget/src/image.rs": { + "bytes": 12361, + "sha256": "dab4c761c846ba2006ba5298c9b3573b0a2787272b2f74376cde6c35ae7f3694" + }, + "crates/vendor/iced_widget/src/image/viewer.rs": { + "bytes": 14066, + "sha256": "ce372e8ba97248cea915abd52046020c0f5c318efc2af1d7f4d74958b14629bf" + }, + "crates/vendor/iced_widget/src/keyed.rs": { + "bytes": 2840, + "sha256": "f1e572a13d0717816a6d4467d14f07fa613956a6bf9a2b234d79789b0bcb1d57" + }, + "crates/vendor/iced_widget/src/keyed/column.rs": { + "bytes": 10710, + "sha256": "09adcae868d561443e8d8b6f3cdfefd6831d8e94adf2b22c551c85b9669eb620" + }, + "crates/vendor/iced_widget/src/lazy.rs": { + "bytes": 11530, + "sha256": "8c127fcd044cf887564856ce3a38ecf41ff3ff24d832d72095ec4e92f9907673" + }, + "crates/vendor/iced_widget/src/lazy/cache.rs": { + "bytes": 377, + "sha256": "59445853fb59941d73904689e5ee34bc6e9f7159c58bbea0cac8ef87f69a1b10" + }, + "crates/vendor/iced_widget/src/lazy/component.rs": { + "bytes": 20440, + "sha256": "05e4e793f3462e5f164ef1191a5cf2c6daf32d410aeebbe1bb4e951fb04fdef7" + }, + "crates/vendor/iced_widget/src/lazy/helpers.rs": { + "bytes": 1292, + "sha256": "d6bab15685469619bc3833afacc1df22112adacbdf944d6dbb5ea1b9e5f9d993" + }, + "crates/vendor/iced_widget/src/lib.rs": { + "bytes": 2933, + "sha256": "4da7f1a83cd8edc30bd9983e59ba3537f44a475814d0b9c80dcf7e97873badb4" + }, + "crates/vendor/iced_widget/src/markdown.rs": { + "bytes": 50899, + "sha256": "5269582001d26d79f9de4e6ee60ebed587adf552f6f91c8ffcd757782319d161" + }, + "crates/vendor/iced_widget/src/mouse_area.rs": { + "bytes": 12666, + "sha256": "01686e65f76a4b89356ec6e5900b4411c856b15a56236ed3f6ef410ec3a4983d" + }, + "crates/vendor/iced_widget/src/overlay.rs": { + "bytes": 72, + "sha256": "104d339ba61626ebf0877f5c942e9f67b3db6261d6c9ae5e4805859508e1bc49" + }, + "crates/vendor/iced_widget/src/overlay/menu.rs": { + "bytes": 19043, + "sha256": "89719b05b75b4b1382de49e1220cab0034b155a24d744509ad928a25d69cca57" + }, + "crates/vendor/iced_widget/src/pane_grid.rs": { + "bytes": 42574, + "sha256": "4eb5ae3be82237f5193b550d407c1a5a9f099a4d554442dbe8f51b0b72edb213" + }, + "crates/vendor/iced_widget/src/pane_grid/axis.rs": { + "bytes": 7860, + "sha256": "4e7a842bc60bee64ee86c4d512c0ac034e4c6bf16b1fe0777c16b8a0b5b28751" + }, + "crates/vendor/iced_widget/src/pane_grid/configuration.rs": { + "bytes": 609, + "sha256": "0d70a26e2fcafc1f1403e43f6c0790698940c92a0966d4ea4acec216778d65cd" + }, + "crates/vendor/iced_widget/src/pane_grid/content.rs": { + "bytes": 11886, + "sha256": "13ec6f9517e0cbd7d2a311ef57b963eff2c5fe6f05264cca29e54d14a0ea8151" + }, + "crates/vendor/iced_widget/src/pane_grid/controls.rs": { + "bytes": 1591, + "sha256": "c7eeffce253b8aa0e7853baaf33ab3e926261df7e6b809a566aa17a4e779d560" + }, + "crates/vendor/iced_widget/src/pane_grid/direction.rs": { + "bytes": 186, + "sha256": "f79259afb26fb38f4af7efdaea5fc6527b5426fe195ef89f72d16405d0bc3076" + }, + "crates/vendor/iced_widget/src/pane_grid/draggable.rs": { + "bytes": 290, + "sha256": "3688ee1a543a281c036d2f4f156a3c529263219b50e3945ef50fa4bb63b02ae3" + }, + "crates/vendor/iced_widget/src/pane_grid/node.rs": { + "bytes": 10452, + "sha256": "bcda8a561581849163a3bb75f34d310d6463ff58ea6660e9fa33c8ea22d40c9f" + }, + "crates/vendor/iced_widget/src/pane_grid/pane.rs": { + "bytes": 209, + "sha256": "2305c36971b405368a99394ad6b3cd0cf21638d9933f8cbd445fd764a257a4df" + }, + "crates/vendor/iced_widget/src/pane_grid/split.rs": { + "bytes": 221, + "sha256": "8be472fc48c37055bbb62395839c599559abc3d70cb7806892f869bd95e90804" + }, + "crates/vendor/iced_widget/src/pane_grid/state.rs": { + "bytes": 14109, + "sha256": "a13125af64ea6da1bcb5a13469f4ffdd51106776b27871e10ce3ac037d865b63" + }, + "crates/vendor/iced_widget/src/pane_grid/title_bar.rs": { + "bytes": 20398, + "sha256": "7556b48c85811d64e3c11c4ce901c579568f4ec056ff5f1bd5e38066b2f059ce" + }, + "crates/vendor/iced_widget/src/pick_list.rs": { + "bytes": 27489, + "sha256": "449fbbc5342c2bed227ec2587111ab43dcecb7382001fbcba2fab2651e54b33b" + }, + "crates/vendor/iced_widget/src/pin.rs": { + "bytes": 7207, + "sha256": "f0b0ab2089300c9ceab8f8f3c75cd96409c7a84f3fa26d68645aab3f92247426" + }, + "crates/vendor/iced_widget/src/progress_bar.rs": { + "bytes": 8776, + "sha256": "38e9bb585a9b764ff6cb947e17d65259a6a32cf3a69b23844e4c28e6c5a1c3ba" + }, + "crates/vendor/iced_widget/src/qr_code.rs": { + "bytes": 12261, + "sha256": "712482a35eab47e0559fafd5452a2b934c5fbc5ae07c14c0c71781713b4dbfcc" + }, + "crates/vendor/iced_widget/src/radio.rs": { + "bytes": 15482, + "sha256": "7b0f42744416e07f8af9409c5bae997b07a0653492ccc2e71911bdeed9c747b2" + }, + "crates/vendor/iced_widget/src/responsive.rs": { + "bytes": 5358, + "sha256": "2a3ff68573825a1407e3be27adbfb4851c7aae7cd100c7a0b670911bc3a08393" + }, + "crates/vendor/iced_widget/src/row.rs": { + "bytes": 16919, + "sha256": "2566e8868bced1a6b7d455c9f1ab61c7025772b27bf843ee806d5c7806fcd44b" + }, + "crates/vendor/iced_widget/src/rule.rs": { + "bytes": 8599, + "sha256": "b7b23722d314864d712e21c8ef260d2b747d0ac795bb3263a0a762c38467d6b9" + }, + "crates/vendor/iced_widget/src/scrollable.rs": { + "bytes": 80594, + "sha256": "a176ea537bbe717550fe4dc17af31c801d9ec118edbcc44aab59ea1a78d1a7a2" + }, + "crates/vendor/iced_widget/src/sensor.rs": { + "bytes": 12155, + "sha256": "cb95d1220fa5663b7a148d6b159081da2200debe6fea982a04f48e27dd072973" + }, + "crates/vendor/iced_widget/src/shader.rs": { + "bytes": 5094, + "sha256": "8753516e6a6b422a76632d07c716e047ff9b5e8f32bf9a9463c612c292306382" + }, + "crates/vendor/iced_widget/src/shader/program.rs": { + "bytes": 1761, + "sha256": "4ce3711b2899b1c1d53544ff0c1f823e108542d356d5e374cb7b149c776d6918" + }, + "crates/vendor/iced_widget/src/slider.rs": { + "bytes": 21695, + "sha256": "778d6a4c37b334cba62ada5f4958e5909a91404961fe5c1104acd9dad12a18d5" + }, + "crates/vendor/iced_widget/src/space.rs": { + "bytes": 2436, + "sha256": "40839bf1dfa93e02c9e3d7e2ab5bb41b328b1fe863846f6f1feca11d9885453b" + }, + "crates/vendor/iced_widget/src/stack.rs": { + "bytes": 12078, + "sha256": "b12776ae1dd46ba95ca0ca52f6a095be1e0121c51f3180ad7cc202cd55764594" + }, + "crates/vendor/iced_widget/src/svg.rs": { + "bytes": 9618, + "sha256": "a8167836a19806f4c2cc633a265953b8c4348f279082d7577b69f817e3166aeb" + }, + "crates/vendor/iced_widget/src/table.rs": { + "bytes": 20855, + "sha256": "373f600db363090412e6bfc9c338bd6bc9516e78f7a43fb00f20d5ed21eb625c" + }, + "crates/vendor/iced_widget/src/text.rs": { + "bytes": 902, + "sha256": "a7d125fc7d07ba51e05f86ca97f4ffad5db37ef2e6d4320bbcd8475d99d51ed3" + }, + "crates/vendor/iced_widget/src/text/rich.rs": { + "bytes": 16952, + "sha256": "7deababcbe5f0294b605822fe2651eb0fee6e9c1bee37883dbc570481df6a0b9" + }, + "crates/vendor/iced_widget/src/text_editor.rs": { + "bytes": 48646, + "sha256": "70e2b5fa29b38ceb9b7688982712647bbb60b751681bfb6c3ca6bcacb51a08e1" + }, + "crates/vendor/iced_widget/src/text_input.rs": { + "bytes": 61293, + "sha256": "7cd64b0f79c6000f2985cc290642698a814be5dcd7b7a56ae463b13ffe5c3b47" + }, + "crates/vendor/iced_widget/src/text_input/cursor.rs": { + "bytes": 5540, + "sha256": "eeb9241a25110583fd2e169da47834aeb39fd55335ea3b77aa3d1e43016c28d8" + }, + "crates/vendor/iced_widget/src/text_input/editor.rs": { + "bytes": 1960, + "sha256": "cd0e0168e9de1ad2599120a4dda8a5a373813a4ffc3a3c035929d18144544408" + }, + "crates/vendor/iced_widget/src/text_input/value.rs": { + "bytes": 4403, + "sha256": "03fba71d34f2c31df15431ac7e22f854b655336f86ef854ae34cc1e0bfbcefdd" + }, + "crates/vendor/iced_widget/src/themer.rs": { + "bytes": 8557, + "sha256": "4b87e866b8f2a4420f30674ad64c605d24e3004f867bf95a1210b5338ed7a81e" + }, + "crates/vendor/iced_widget/src/toggler.rs": { + "bytes": 18223, + "sha256": "70f1ef0ee8e2b6c0d7b93095eab1e615783bb81d945b4ff066af025dde8f5dcb" + }, + "crates/vendor/iced_widget/src/tooltip.rs": { + "bytes": 17259, + "sha256": "7fcaa3899ad1a946bb1a51d34d344497005aaccdf825f510e98105d1bc40bc63" + }, + "crates/vendor/iced_widget/src/vertical_slider.rs": { + "bytes": 17741, + "sha256": "75777345ced463b4a3b059fbe6c59efc952169987154b0597d87ce4da2076e3b" + }, + "docs/acceptance.md": { + "bytes": 3463, + "sha256": "32ed3e4fed63f5c464e0937221f401d71623b276dd70f083531ac6ccebbf9050" + }, + "docs/api.html": { + "bytes": 74641, + "sha256": "d06e32b3da838e519630a4151111bb40a383ccf294eb18443118a7498700a993" + }, + "docs/api.rst": { + "bytes": 43564, + "sha256": "d78f138d011dd0af28bd53a65d1d93381d031f680191779d4f3030a264eb2e74" + }, + "docs/architecture.md": { + "bytes": 10974, + "sha256": "a50bd1645222ef3e574fa07a6ab9aa213431ee8833c6c0158ad91ce0f799543c" + }, + "docs/language.html": { + "bytes": 191933, + "sha256": "77c14e548f5d2deea0c21a3a931347e1c87449f5be8503f6c51ceaef37be9a33" + }, + "docs/language.rst": { + "bytes": 146279, + "sha256": "7117a66e16b34cd6ce033532e984c7ca49b4611e296fb1c46c5f737fe9169ec6" + }, + "docs/linux-packaging.md": { + "bytes": 8671, + "sha256": "41a1bb1095801df37a1411341c5791d55ea8c365739314d599a60269ac4bc7a0" + }, + "docs/openapi.json": { + "bytes": 348802, + "sha256": "2ad69da52fb574435525990c906948ce54b1efd9247cdde8726969f382731790" + }, + "docs/research-v2-architecture.md": { + "bytes": 40445, + "sha256": "1b3809f2dabe6b1805dbf3d3f2625c471cd21356c7bd603916466a8c4ced8090" + }, + "docs/research-v2-semantics.md": { + "bytes": 31535, + "sha256": "b7ec3677300248926a88b84da3e7ffe52f5b73524c7712c927a0e951569db04f" + }, + "docs/research-v2-service.md": { + "bytes": 36209, + "sha256": "4828df5bc679e7c71f49b4b85c9e7dacbec52f46d416fed48af70e08886d4e4e" + }, + "docs/research-v2-ui.md": { + "bytes": 3245, + "sha256": "894e721b59a8ca85f0d95b6706ed5f130a0372def460cf33aeefc9e5e38cde0a" + }, + "docs/scope.md": { + "bytes": 16690, + "sha256": "a1d0cfbfc06fc8bc2bf0f71343aacc8853cbbbfc00a1fc9852ddf41c8245ffca" + }, + "docs/server.md": { + "bytes": 23361, + "sha256": "4eb24937d424ba8b384e1734e02a6f8e963ae11d2715889c92b14b39ff2e4a48" + }, + "docs/source-cli.md": { + "bytes": 3374, + "sha256": "34f12b72cc6d6ead4f47c7818c1897db592f8ad493c70776db2ab48c24d8f051" + }, + "docs/v2-interface.md": { + "bytes": 11807, + "sha256": "c7fbaf0ebd3fc9f6c11f818db2b0b9d68a33f580cfced22bde83ce08ae790bc7" + }, + "docs/validation.md": { + "bytes": 6505, + "sha256": "f46ea950ed1a54409aa07c4f9e90f39f8e2d00c1dd1b6cb805caa3993845967f" + }, + "docs/windows-packaging.md": { + "bytes": 10406, + "sha256": "2c852ff7041713ffbff3c951ac98df647c51fb70b0feba9000f7bfd5bc2703a8" + }, + "examples/coastal-rescue.arcadia": { + "bytes": 33107, + "sha256": "7e183ef9fd8154d3074303f1956cd0d9871c19e3ffc630d333b0bffbd6e30e17" + }, + "examples/coastal-rescue.json": { + "bytes": 49715, + "sha256": "242f0b031229a87af5c4ebe5d651defe59c191a6b7a69dc0a77f7e203411f195" + }, + "examples/legacy-v1-coastal-rescue.arcadia": { + "bytes": 3449, + "sha256": "4dbbbdf8bc7e6cb8e0a81dd678069dae585a9e5187c59bc3665e1fdab3f1a86a" + }, + "examples/legacy-v1-coastal-rescue.oa.json": { + "bytes": 5976, + "sha256": "fd0e9dedfb7adb922dc0575cc2b820d40344d639f1d8052bd70363aa59de9c24" + }, + "examples/release1/README.md": { + "bytes": 11137, + "sha256": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5" + }, + "examples/release1/evidence/repair-check.md": { + "bytes": 1419, + "sha256": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5" + }, + "examples/release1/expected-diagnostics.json": { + "bytes": 288, + "sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e" + }, + "examples/release1/model.json": { + "bytes": 134520, + "sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727" + }, + "examples/release1/model.rs": { + "bytes": 28762, + "sha256": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5" + }, + "examples/release1/oa/activities/activity_oa_restore_work.arcadia": { + "bytes": 1130, + "sha256": "21cbd7176af8fad68453306f2371ff03e127a994b8796cdf044567332de9686f" + }, + "examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia": { + "bytes": 550, + "sha256": "23cabc213c264a16c8595414db6acb09248c1cef9cd699f1b0316e5bb82c062a" + }, + "examples/release1/oa/behavior/process_oa_field_procedure.arcadia": { + "bytes": 1178, + "sha256": "9c52086759b7344b4809ceb8d372f65647b16fc24153a03b887faa378c37fe96" + }, + "examples/release1/oa/behavior/process_oa_response.arcadia": { + "bytes": 2560, + "sha256": "675798bd19e6b40d03e7076b0e59b110cdaaab7b6d16c47c3ac80fa960072e84" + }, + "examples/release1/oa/behavior/scenario_oa_conversation.arcadia": { + "bytes": 1515, + "sha256": "e217e5394a0771dc16206ed9437a231da27905d2f19be64b3f48281c0a32fb6b" + }, + "examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia": { + "bytes": 1121, + "sha256": "c5a285f2d7c61bd2a136107f267fbf9cddb1cba3f63ff36a6f53f7ab98f33df5" + }, + "examples/release1/oa/behavior/state_machine_oa_monitor.arcadia": { + "bytes": 1358, + "sha256": "e7349e1e5336282eed3f5a103129db33d3ae4ed500788ee7129e8fc193ac57f6" + }, + "examples/release1/oa/capabilities.arcadia": { + "bytes": 630, + "sha256": "26a3d9b615a3251ff010e0b47127f2d16bc2dcd45ed95967208b0076d725843a" + }, + "examples/release1/oa/model.arcadia": { + "bytes": 531, + "sha256": "069a5eaaf525089935cc9d73ae7b916866ed78c6534705d75739650ecdea7ada" + }, + "examples/release1/oa/participants.arcadia": { + "bytes": 1580, + "sha256": "591a75a9ecad280302be4d4ae0171ad616377ed11ead5e233848ca2dc0a71819" + }, + "examples/release1/oa/types.arcadia": { + "bytes": 554, + "sha256": "f02c391d584674c879f662a418abba3a220f1fee88471ca49322ac9873de628d" + }, + "examples/release1/project.arcadia": { + "bytes": 1958, + "sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda" + }, + "examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia": { + "bytes": 559, + "sha256": "5c414fbdaabf9f9e402b0441fcacf1cacf4dbbc6c822162f028de35abf59be2d" + }, + "examples/release1/sa/behavior/process_sa_field_procedure.arcadia": { + "bytes": 1187, + "sha256": "ece846fdf93857e71a9ab0efcbdf64a5c41c71180f62afa3f79077fffb53b8f2" + }, + "examples/release1/sa/behavior/process_sa_response.arcadia": { + "bytes": 2569, + "sha256": "af7fdda280a917cf626f6e077162d481f9aaad280f8e81330c07d7fe384b1c52" + }, + "examples/release1/sa/behavior/scenario_sa_conversation.arcadia": { + "bytes": 1847, + "sha256": "01339699ec3ee409418d9bb232542f2bcd9419f1e67b183e77700c56bb9f71d2" + }, + "examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia": { + "bytes": 1116, + "sha256": "5dce6b00a3a441162e8dee3e23d9ca5ab7bed4a24de669b1fba03664a3086dda" + }, + "examples/release1/sa/behavior/state_machine_sa_monitor.arcadia": { + "bytes": 1358, + "sha256": "17dbcfa5bbfd10fc8e23a325fc950ce853c219b4aa89c74f96278cbb3f34acc8" + }, + "examples/release1/sa/capabilities.arcadia": { + "bytes": 630, + "sha256": "81706a0ca9d7db2aa0350888b9c00d853885185db2e6b90557d3d51a11c3d645" + }, + "examples/release1/sa/functions/function_sa_audit_thresholds.arcadia": { + "bytes": 280, + "sha256": "27f798f78f36e72b38edab3725af2feb0eaa010fd52a60b5c6b6bef57ad3b686" + }, + "examples/release1/sa/functions/function_sa_restore_work.arcadia": { + "bytes": 2251, + "sha256": "357c6acb80f86b4e8d8f5c98f9f79d140f85fbe09f3406b41b7ac6eba464ec8c" + }, + "examples/release1/sa/model.arcadia": { + "bytes": 531, + "sha256": "805b90262e27090ae7da81a51bcefa49c97fb0b3d3220b4a72323b4c4351c395" + }, + "examples/release1/sa/participants.arcadia": { + "bytes": 3604, + "sha256": "ca144d6a8f45fe720b5d18c7b59c7671444baa8c37e5ce9117bcba34b3a6aa84" + }, + "examples/release1/sa/types.arcadia": { + "bytes": 903, + "sha256": "540ddbc2bcd0ccf45298e3ea5660637341aff63ff65fd87ac2f26469d1adc064" + }, + "examples/release1/traceability.arcadia": { + "bytes": 28041, + "sha256": "474bd28d955ff7af7722f6e9a23220d816879e3f46202e79a233544b9c593116" + }, + "examples/release1/transition-before.json": { + "bytes": 35801, + "sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d" + }, + "examples/release1/transition-request.json": { + "bytes": 4276, + "sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a" + }, + "examples/release1/views/diagram_oa_allocation_view.arcadia": { + "bytes": 1786, + "sha256": "c45d7467839219d894804c1ef2d22ff4d4a5a84a6a273c526bafd4c1facde624" + }, + "examples/release1/views/diagram_sa_allocation_view.arcadia": { + "bytes": 1797, + "sha256": "352a3ca745517592b5c9b1239b5770f62f0466a329a91fd03457ef2b53c06ed6" + }, + "examples/release1/views/diagram_sa_capability_view.arcadia": { + "bytes": 1286, + "sha256": "8f18fcccbcddcf3c1ea0ffe727b5b23b77fc148c5da4fde953abe681e4c20cd2" + }, + "examples/release1/views/diagram_sa_service_view.arcadia": { + "bytes": 1960, + "sha256": "4c72cb7593d9c4be8d2600b0656a7a3a6ce84c45bb2f3e58a4ee5ae3bf91913c" + }, + "examples/release1/views/diagram_sa_state_view.arcadia": { + "bytes": 1576, + "sha256": "dd1bea81ac7dc549a53f9b190be6f13052e00ede345bd180444cc31edb892230" + }, + "examples/routing-workshop.arcadia": { + "bytes": 66431, + "sha256": "dc6b75da72884d399132ed9e300fa5a26b73d802bb526c9af986ae9eeb0a646a" + }, + "examples/routing-workshop.json": { + "bytes": 100281, + "sha256": "8592714f75b271133f21ce017d87896b6cf91ea0e4837f1c2ed453e21be26b14" + }, + "examples/source-project/oa.arcadia": { + "bytes": 2142, + "sha256": "5f60642f211b2d352a951092faeeb7503d4fb7f6f7fca99e810347231101d31c" + }, + "examples/source-project/project.arcadia": { + "bytes": 606, + "sha256": "787f6b67b179ffd221f7e5faf1a4edd74cfd116aa1e3b5e9ed9f22fbff612d8d" + }, + "examples/source-project/sa/assurance.arcadia": { + "bytes": 682, + "sha256": "ab76044836e4dcd42a82d316846ec87da2108939aa2037640c5bca1d2ecdee0f" + }, + "examples/source-project/sa/participants.arcadia": { + "bytes": 1231, + "sha256": "1abad579812e77877b45a3d8e3c3e9da734be2d04a0254cc730a5257ebd2dc88" + }, + "examples/source-project/sa/reporting.arcadia": { + "bytes": 2839, + "sha256": "f0b7443f728912ca163de39b6055876b533a77feaa3219b2d40a2dd65712e058" + }, + "examples/source-project/views.arcadia": { + "bytes": 3508, + "sha256": "e29ad3f406a6a50d2f0ae2ca2a53ee3b0c18222cd4098704a46d4f48b64ea17f" + }, + "examples/text-first/README.md": { + "bytes": 1367, + "sha256": "4ec8250d5ed556f1d496e8c85137e973e802ecb11df1a36488411e8cdc5a9f9e" + }, + "examples/text-first/project.arcadia": { + "bytes": 480, + "sha256": "f441535c623e0cf20e1875eb2cd33b587ed00e078c37af39cb36d89bb5edc19a" + }, + "harness/fixtures/basic-editing/README.md": { + "bytes": 12012, + "sha256": "3545d9695e7c62e0caa818efd7f4dc426f04876a3626e0f3b1bad606b258cb26" + }, + "harness/fixtures/basic-editing/base/expected-baseline.json": { + "bytes": 4524, + "sha256": "f239e728d650b0af32bb8f3dcaf6b27c99b9c5041132ac14e51756ea6563d978" + }, + "harness/fixtures/basic-editing/base/project.arcadia": { + "bytes": 3077, + "sha256": "045cb047d7bfbca72c3fb30db4c81a30ad463d3af7fc51604906ad7fa75a1ff8" + }, + "harness/fixtures/basic-editing/base/rationale.arcadia": { + "bytes": 57, + "sha256": "c01867c9dc0d40726853119fac3c6ad24a40d8d362a93a8b9d20df04ec2e273f" + }, + "harness/fixtures/basic-editing/manifest.json": { + "bytes": 447, + "sha256": "d23c21b02064ddb85e051e10e16ff089daba8d9910bb1a695dfabe2db3757734" + }, + "harness/fixtures/behavior-creation/README.md": { + "bytes": 5954, + "sha256": "9e5fc751424d12cfe76a84abb1da45b2d38947409179451bb90ffe7e7ddc8723" + }, + "harness/fixtures/interface-operation-probe/README.md": { + "bytes": 2604, + "sha256": "088f0bdb3375deb8f2c7e3b749ca8d210ea828ca4dece69b3871b674e3110175" + }, + "harness/fixtures/interface-operation-probe/hashes.json": { + "bytes": 977, + "sha256": "f6d3e8aa42f40f64ce2ecba7b9cf112eb9091f3cccde3a592b0434e834cf2a6a" + }, + "harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia": { + "bytes": 4218, + "sha256": "c0fe9570684c0061efc4cad65e339fede6961b04ca90c6f324c787bd65e61226" + }, + "harness/fixtures/interface-operation-probe/output/after-reset-deletion.json": { + "bytes": 7835, + "sha256": "93187f7a1b476a44185725c6b15f2e1767482b6911ab1280144348d033eb51f0" + }, + "harness/fixtures/interface-operation-probe/output/boundary-only.arcadia": { + "bytes": 4771, + "sha256": "02f85e39957ebe6d56336fa729a5c8b10f51865face9d3b2c9679200571fcfc7" + }, + "harness/fixtures/interface-operation-probe/output/boundary-only.json": { + "bytes": 8840, + "sha256": "756f9710336e6734ce3592ca774c6f104bba5a37c9e165540eb39326682393d1" + }, + "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia": { + "bytes": 8635, + "sha256": "ac5b629ba5596464f9f8a975b8bc2764e3007ceb7342de3f828d4b18e4f069b7" + }, + "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json": { + "bytes": 16160, + "sha256": "78a47c978dc667aa1c45af711f06aa2a0d3c581003013e30c3e02fb0faae2e64" + }, + "harness/fixtures/interface-operation-probe/probe.rs": { + "bytes": 11274, + "sha256": "72b1b443e25eb20420dec4477b0fb5ef5d1c705e3882c81df88dd578ab96a426" + }, + "harness/fixtures/release1-native/README.md": { + "bytes": 2564, + "sha256": "5f5ba040fd2df9261a29bcab3f9a7ae27c3683265411ad3dda0b467d5df8590e" + }, + "harness/fixtures/release1-native/artifacts/README.md": { + "bytes": 4155, + "sha256": "24200c84ccc2ef3e17c029744074e5cea785188f5549ac54e44cf89682643f81" + }, + "harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs": { + "bytes": 9275, + "sha256": "444f360b78df52c5e42046f77b148781e0c9bddc2a09c0b7bd65c7a4f376cfe6" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia": { + "bytes": 7768, + "sha256": "84b22e95c213018612e6e8a84cad68b35a8b495ff3515b0f978c5e7a3d196d79" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json": { + "bytes": 7330, + "sha256": "13a6f99ec2df1dd35ee8b2613f183918cd9537eda4a5da085ad52e65c033db59" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia": { + "bytes": 1446, + "sha256": "1e0d662ba33071b122ad9206d2d559a8e748c9431eb3ac1b759bde3760ca877c" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json": { + "bytes": 2216, + "sha256": "fa6a2e3643c0f909b92407abec3edc897033162eeaa501f8d4616f79d41fa044" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json": { + "bytes": 567, + "sha256": "65cb70b0d697a055363ba01bf30bed7e412d43c9978a033549c378d117719ae7" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json": { + "bytes": 3991, + "sha256": "ed905267263e7f8a0cd2dbe833196b30118b6a246815216932fae661ad650699" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia": { + "bytes": 6837, + "sha256": "e50fc462ec22dbf98011929931dd2489430001c3e6433608685b9d88bceb9acb" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json": { + "bytes": 6433, + "sha256": "f27a7b38d59c7cefe58cb36783d3d4dd8d8ae1168d9aed1eb61d343c18758908" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia": { + "bytes": 1807, + "sha256": "eba2aec39e6efb405eace4bc8ab0c5563907c26d449e71a4aa750df9f73233b4" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json": { + "bytes": 2673, + "sha256": "2ef777f19b94f08b5e5ca2ef727777897e1018f1426eee291408b3c69101f596" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json": { + "bytes": 519, + "sha256": "3af7232ed3056ca84dcaf0f266ead3faaa512a0dc0051c7ba68791c20dbae0cf" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json": { + "bytes": 1246, + "sha256": "8af21b00b62e620256fbf50e47c6c5836cdb6922d9549cc4b3d86bc03b008073" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia": { + "bytes": 4200, + "sha256": "20bb30473830e44228958e78fb7480269cdcc0580c6c90c718da75f34ca5a43f" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json": { + "bytes": 4552, + "sha256": "44f4b05db49758dcf344788f57cecc0250fd029099b1c54e4e1e3e972f6cf039" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia": { + "bytes": 1808, + "sha256": "b0fd031671c9f755756922ec5c40a0ce3f40164d22c3c389aec9fd0705344da7" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json": { + "bytes": 2673, + "sha256": "1acf81e08c51c85aceb14a3efbc39ec680552a19c7824aab4611ac3cf71b9c6d" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json": { + "bytes": 610, + "sha256": "a07978f4b0a3ca83e5ae5f35e7b66b26473b24431fc90e5889d755f1816d52dd" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json": { + "bytes": 2352, + "sha256": "5ff844a038b0030adb5a83d13d81e60f30627543388f6199b6a0e026fc42ea58" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md": { + "bytes": 4075, + "sha256": "a51ffbc45b6c172cbfed3b16c249296e6a28d457fea57393dd958cdcfbafbca1" + }, + "harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json": { + "bytes": 2367, + "sha256": "3ad8b7428b3ed7585a91e7f5d9d5f4880caccfd0b08dca25d05af523e62e0a10" + }, + "harness/fixtures/release1-native/communication-means/README.md": { + "bytes": 15091, + "sha256": "015c16010c212af283f9def118c6d263ba65a3bc6e81a750f180624ca69bb311" + }, + "harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia": { + "bytes": 6098, + "sha256": "a6976e7c21031e7cc3cb731e20c3075cf89cf85144dc8d3f52930f8521ea196f" + }, + "harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json": { + "bytes": 10480, + "sha256": "60c4419415c546f06c9972ac7bb0e530ada7b30a200b59e75ddda231a24f9a3d" + }, + "harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia": { + "bytes": 407, + "sha256": "3c41459f2b0706a767988738bf8126f76540415bb5940d9c5db19cbe8c18182d" + }, + "harness/fixtures/release1-native/communication-means/cm-create.request.json": { + "bytes": 1331, + "sha256": "b16e673b29c96b5aed681389ff7669110abe552939e86dd0ec6011c9a45ed860" + }, + "harness/fixtures/release1-native/communication-means/cm-draw.arcadia": { + "bytes": 5239, + "sha256": "a511a4d817d9c5c80233bce83e84a7056b7dce7afd9fcc25696e333f8f4df055" + }, + "harness/fixtures/release1-native/communication-means/cm-draw.json": { + "bytes": 8424, + "sha256": "7140114926228b1c378c450535f7a70992c71a879a8d2270c49e8d5f04a2f484" + }, + "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia": { + "bytes": 5710, + "sha256": "910e6dfe609467047ce629ec9c626f4e927cada74bfbfa164414a8fe6783ea87" + }, + "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json": { + "bytes": 9868, + "sha256": "567259e20c9f6278776e8fa99b8b2fdbbc9a883ad875c5d39dae1b53de3a2924" + }, + "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia": { + "bytes": 407, + "sha256": "997f72fe9258012df66e88344cf39c1a2cebf2327f604056b67f7f2a8ebcfbfa" + }, + "harness/fixtures/release1-native/communication-means/cm-reuse.request.json": { + "bytes": 1127, + "sha256": "cb3fe05859cfffec6f8846c19983dc462413bc0ea4f6e19204f09c259919c4b1" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia": { + "bytes": 8209, + "sha256": "ec826b9b84616507490ac30e4d57100dd6338dba2f7071e879a8fb58820a85c5" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json": { + "bytes": 19735, + "sha256": "ef07e4d2d3fc36004ff01739838281581d9a703c747611eacc01bb9cc672489f" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia": { + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json": { + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia": { + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia": { + "bytes": 9827, + "sha256": "86dd8a355f6c9dadfea56d06e5247c9b0f820819d7df17ef5690671c7c81324b" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json": { + "bytes": 21877, + "sha256": "980489ec2c483d4f9efada61358c1f29bafb2572fd5b804c656b91b8e9bb4f9a" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia": { + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia": { + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia": { + "bytes": 7698, + "sha256": "1abc4c4cac37cfb06ed3b12a115f9e7e6a2d3affd34b25c355309e5ee465fa40" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json": { + "bytes": 13130, + "sha256": "a5cf4cb2ef390113dd752f4054d43fb789da2ca77799efb5782805ae8aa2dfd5" + }, + "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json": { + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + "harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia": { + "bytes": 5454, + "sha256": "b761c4c2af5f7064aaf1f887ffd6094eb6ae834b2c8057ecdbc4aa97c042066b" + }, + "harness/fixtures/release1-native/communication-means/cm-transition-ready.json": { + "bytes": 8852, + "sha256": "2536b000de361fb131787e32b466b9123d4e98abf00c099ad32f6cb8e7fb530b" + }, + "harness/fixtures/release1-native/communication-means/generate-communication-means.rs": { + "bytes": 28035, + "sha256": "003521404b702332082e01f2be7676fda1dd7b5bcf220bb81d25120c8984c75d" + }, + "harness/fixtures/release1-native/communication-means/hashes.json": { + "bytes": 3133, + "sha256": "39b4dc02fc8b074015c59354fd1c1061c25832fde25065c7151f36eb0455e6f6" + }, + "harness/fixtures/release1-native/communication-means/manifest.json": { + "bytes": 1237, + "sha256": "fb921f42f2c4a4f8a67d1266f17f8c7750b6a2619554b713aa92b17918f0c43e" + }, + "harness/fixtures/release1-native/dense-40/dense-40-oracle.json": { + "bytes": 26264, + "sha256": "74edca148583f42c269e51f135662f65ef1c9ea1c94516d63e2a5091967dc99b" + }, + "harness/fixtures/release1-native/dense-40/dense-40.arcadia": { + "bytes": 28947, + "sha256": "00b87736b82c5afd33b311df7aebe9a6c8c51b6de66221181b5ea8bc8ca221a3" + }, + "harness/fixtures/release1-native/dense-40/dense-40.json": { + "bytes": 54938, + "sha256": "5dd4f22453330cab3abc059881660190e689a22619853f77cf6d0b52735d97ad" + }, + "harness/fixtures/release1-native/semantic-attachments-native.md": { + "bytes": 2624, + "sha256": "28906924b05f75bbf432389517aef65f1df474d2f2bff79a3586564f7f7328ef" + }, + "harness/fixtures/release1-native/semantic-attachments.after.arcadia": { + "bytes": 6795, + "sha256": "9d084dd75a0251d349cc31f9ee98df58d0bc76859dc3cb1503f242e2f759888a" + }, + "harness/fixtures/release1-native/semantic-attachments.after.json": { + "bytes": 9618, + "sha256": "f09be41afd85c18fd52fdf997e9b81c4b7cb2c41e78d167abcd14caea8110333" + }, + "harness/fixtures/release1-native/semantic-attachments.arcadia": { + "bytes": 4990, + "sha256": "8740f27be99624013d9be876639c0411e1b8c14f0f9a843f43dc71d769efd79a" + }, + "harness/fixtures/release1-native/semantic-attachments.hashes.json": { + "bytes": 963, + "sha256": "00fc9e2c709bdcd9ee98012176cb0edb635c112153d963cd169bf4bf69a9e010" + }, + "harness/fixtures/release1-native/semantic-attachments.json": { + "bytes": 8146, + "sha256": "9c188f2890630c7feec2be719a7104af9690da7ea836df18fa78da3ff2acfc1b" + }, + "harness/fixtures/release1-native/semantic-attachments.manifest.json": { + "bytes": 1045, + "sha256": "46b40f221ffad614733b615208814ace63947e96b80bbfdec63853ccd4f4e8c8" + }, + "harness/fixtures/release1-native/semantic-attachments.request.json": { + "bytes": 3075, + "sha256": "b57f656f0b9cf2c595e83150c683301909e20f15684d35d108d3c57ee0dc5909" + }, + "harness/fixtures/release1-native/semantic-batch-native.md": { + "bytes": 2637, + "sha256": "dddca2b85f892b05881c75ef3cd62437396cc086a09909b621e02bad0cbe56ff" + }, + "harness/fixtures/release1-native/semantic-batch.after.arcadia": { + "bytes": 3134, + "sha256": "d3315bba762395205ae761e6e64029e8b4070c15a1299c6114075581db7b0974" + }, + "harness/fixtures/release1-native/semantic-batch.after.json": { + "bytes": 3927, + "sha256": "8f36119c7e893a4af374b13b85b1c2cb3df0196df8d06a551386628fc8b8646d" + }, + "harness/fixtures/release1-native/semantic-batch.arcadia": { + "bytes": 1787, + "sha256": "5e1f433e04621424edac43e53411624059e873f88dd531bfdb39093953485ed7" + }, + "harness/fixtures/release1-native/semantic-batch.hashes.json": { + "bytes": 933, + "sha256": "1bd87700b04e4f169ab5f8aceaa3b0c3ec3a1cc43e1bce135a1cee506fc75d38" + }, + "harness/fixtures/release1-native/semantic-batch.json": { + "bytes": 2816, + "sha256": "1972242d2a8677d004dc3af1081feb292de65fe26128d877dded8661f9906619" + }, + "harness/fixtures/release1-native/semantic-batch.manifest.json": { + "bytes": 647, + "sha256": "1e440efa02e2ccc27585d9a49c7864d53e3e3e7645ccb219f040e00d95331b8e" + }, + "harness/fixtures/release1-native/semantic-batch.request.json": { + "bytes": 2197, + "sha256": "71f23474614aab90398ce24f00f2cf61d6cb9a3a34faa3e12e0c8104e0a71073" + }, + "harness/fixtures/release1-native/semantic-keyed.after-repair.arcadia": { + "bytes": 15635, + "sha256": "e3ce0347746584f503b825a8b6bd23a7eebf0827940112ef8f36d500c1d0a043" + }, + "harness/fixtures/release1-native/semantic-keyed.after-repair.json": { + "bytes": 25736, + "sha256": "29736f2a619f7f2d0e795229f6e8c3645a1b4eca6292043cd604aa01b1ec9170" + }, + "harness/fixtures/release1-native/semantic-keyed.after-take.arcadia": { + "bytes": 15631, + "sha256": "9ab573c92be66e3bae659dd826923ba6066979b1651edda7bd469056632cd77e" + }, + "harness/fixtures/release1-native/semantic-keyed.after-take.json": { + "bytes": 25736, + "sha256": "828c990c6129692cd31c7f6ed0dd6c952f9cfffdd90bc80171df82d4d929afba" + }, + "harness/fixtures/release1-native/semantic-keyed.arcadia": { + "bytes": 15617, + "sha256": "49214be2ca9c8bb6195c3b39737c053847021839f70f0bb2921d35a1fbd2ac83" + }, + "harness/fixtures/release1-native/semantic-keyed.invalid-request.json": { + "bytes": 2225, + "sha256": "8b02d4739b1be2c8a64da76423b4a6cae7d89c60f4d30723b94f1da3779cbcca" + }, + "harness/fixtures/release1-native/semantic-keyed.invalid.txt": { + "bytes": 78, + "sha256": "aa536e18bc65325163e9b645a4ad031ab599fcb05948dcf741b725d0f0c8b602" + }, + "harness/fixtures/release1-native/semantic-keyed.json": { + "bytes": 25722, + "sha256": "995dd72f5c1f07f998233ae9847e337cb82de0c2a9053d3905be021d5f8124b9" + }, + "harness/fixtures/release1-native/semantic-keyed.manifest.json": { + "bytes": 1059, + "sha256": "ad315359bfd68ce0d265976dc9e4ea6a502ebbb87d7339dd1af5ef27dd01e433" + }, + "harness/fixtures/release1-native/semantic-keyed.repair-plan.json": { + "bytes": 15016, + "sha256": "27bf316ad66ffa91f6323e75b0c280baf5caec298645793dea72c5d42aa3b782" + }, + "harness/fixtures/release1-native/semantic-keyed.repair-request.json": { + "bytes": 2225, + "sha256": "f4f6b5480ec8927033ea469ea82c1e8411cb6dba95d2da680f15bc193914c9b4" + }, + "harness/fixtures/release1-native/semantic-keyed.steps.md": { + "bytes": 3897, + "sha256": "7d7e38d93641a5f22a2738baa4dbacc392eebb477a855cdce0641961980a19ad" + }, + "harness/fixtures/release1-native/semantic-keyed.take-plan.json": { + "bytes": 18557, + "sha256": "74dd68ba686b8928054480f0bfed55aad91f6f1208803399dc9878e894f57ecb" + }, + "harness/fixtures/release1-native/semantic-keyed.take-request.json": { + "bytes": 1508, + "sha256": "d19a53002a6bf95af3123a3ef9f78baaf7ce1085d4f06f0f4a9d5e5d5accbc0e" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.arcadia": { + "bytes": 6935, + "sha256": "90277f1780d43a6f0b2184bf00b87e7a4f3518d67c6f10d4793daafe1aabf493" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.json": { + "bytes": 11286, + "sha256": "5a3c8494f6976c23b990c14b40760fb2f32ca9973caf6f31dab5632310663591" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous-request.json": { + "bytes": 1379, + "sha256": "6a86fde194faa55f2d1bdb982571e0e711e5401b53a960e6acb8cf3e17f506c8" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous.txt": { + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.arcadia": { + "bytes": 6195, + "sha256": "e7c30c5a5c3ba7209c8e223f02da55821d7af10a2f09844a7065ae6cd8f22aa5" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-plan.json": { + "bytes": 19156, + "sha256": "d1e5c2ed08e0aa6274a0421a39876d1ba99f4e441c7a0a68660a83b9e017da14" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-request.json": { + "bytes": 2418, + "sha256": "433f723252598994dac7184da61f95344b528156cc5a0fba1a7864b2ec9f002b" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.json": { + "bytes": 10441, + "sha256": "9e4e2ce1c7879935ad0860a3ae720506c929a8422b51491b719d88af3fd3ff2d" + }, + "harness/fixtures/release1-native/semantic-ordered-branch-steps.manifest.json": { + "bytes": 1042, + "sha256": "87e8899c7d4e54faedeef32afb7592fb65278d1bd7390850ab198dcf62068329" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.arcadia": { + "bytes": 6280, + "sha256": "6c77ed42bbf9f4ebe08bfa233197a4020e5587ce33747f7914430aa0c662d9a3" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.json": { + "bytes": 10009, + "sha256": "1dd7ca2bcfc84ac23369c506302b39569d2ebc41e5a4edc99378a1fe01923b7e" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous-request.json": { + "bytes": 1390, + "sha256": "e0ea7aeb25687ae63c65128f1aab78273f14030d179d3fbea63fe91ab5430136" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous.txt": { + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.arcadia": { + "bytes": 5529, + "sha256": "563c36162050c8564ffa25a85c726462ecdaebdb87659b3f3610506bfd5581e6" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.final-plan.json": { + "bytes": 18821, + "sha256": "f96c43fc58dbe48a044b628b31bb3f0b59171dad5b6982ec6c34a5597e474130" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.final-request.json": { + "bytes": 2438, + "sha256": "d38fdb13612b07254edad80848f79c1e097d5f868c020feae9ba072aa697a50d" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.json": { + "bytes": 9153, + "sha256": "2ed0daed118f2df13dcad738dab174ad34d44a15921cf8097415e707978b0968" + }, + "harness/fixtures/release1-native/semantic-ordered-fragment.manifest.json": { + "bytes": 1058, + "sha256": "c79bd12a85d8d4f25cc3ee43581b3e5e74ae9ad7e4de0c72764975ba2b89c279" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.arcadia": { + "bytes": 5026, + "sha256": "98020e048ac01f285c6ad8c10ce22bef05e193b5ba0f5558be1fb26a7bf82620" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.json": { + "bytes": 7851, + "sha256": "ea3783992488bc6ee944eb91a26ccf3dbc05a3909528fc32ec8023a76765cee6" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous-request.json": { + "bytes": 1379, + "sha256": "ea3cfd25f7a50481805956fb88ab851516dcd602dd6564502a0cc55f7de6a54f" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous.txt": { + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.arcadia": { + "bytes": 4295, + "sha256": "2b7c3694623bbea3e36dcc6f674d4adcd91e23c8e1005ded21e5a69648a048e5" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.final-plan.json": { + "bytes": 17912, + "sha256": "95a23bd334cffcaef0c3b1012b7555efd2a077ef6f9140d58020910ef8a76723" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.final-request.json": { + "bytes": 2081, + "sha256": "1e2433ed5821bf65443f56cb27381ee935db81b1955d3a539b7952898312a410" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.json": { + "bytes": 7012, + "sha256": "0033f8780c2ba69779b811bca792340c25a092f887e5811c6839f0a691d744f2" + }, + "harness/fixtures/release1-native/semantic-ordered-reverse.manifest.json": { + "bytes": 1046, + "sha256": "e3b4a60a56abee779ca8356605c968aa2d99256a3bd3c5601b14b5c5502a1a05" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.arcadia": { + "bytes": 5467, + "sha256": "e265cab35097acc46f5123107c13bc35b9f71931ff650557fc1c85779141b327" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.json": { + "bytes": 8482, + "sha256": "9b777828d3681138de6bbd08de8bf922bed92c27be60b103d8ff73ce7ddd0a48" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous-request.json": { + "bytes": 1379, + "sha256": "7d124256730bf2feee772c7d0118b87eb288e8aaa9179d69ceb9259dbceb2c71" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous.txt": { + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.arcadia": { + "bytes": 4727, + "sha256": "87a5a3665dab2dd6e595867dcebb1586e93dd067c85d6e24b1a4d3ff0ad6d4ba" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.final-plan.json": { + "bytes": 18803, + "sha256": "1888ca51e0923866a9afea99cad069fe5d3f19f0a1b24328e20ac1b9f52d8bad" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.final-request.json": { + "bytes": 2294, + "sha256": "5070a5b85feb1bc7b1fc648cd5d35ef26663492c8666dcd471950c06ec3a6052" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.json": { + "bytes": 7641, + "sha256": "725b35e36640c81c70c7efea5c3304bf61b49ef9664194ca0d491782cc62191a" + }, + "harness/fixtures/release1-native/semantic-ordered-scenario.manifest.json": { + "bytes": 1038, + "sha256": "1b43abf8fed3061c65a7b7cb103d31ea7ece333a2da83c645f8d228448c4dbb2" + }, + "harness/fixtures/release1-native/semantic-ordered.after-order.arcadia": { + "bytes": 5019, + "sha256": "65a6217f474b5085f37b511151185c2c08ff7a1998e924f1fe01f29c410f6ced" + }, + "harness/fixtures/release1-native/semantic-ordered.after-order.json": { + "bytes": 7835, + "sha256": "8a53bb9b43eab5b6dca5e08deef20c14c96e134b9ee816a99b7960bb5255e008" + }, + "harness/fixtures/release1-native/semantic-ordered.ambiguous-request.json": { + "bytes": 1379, + "sha256": "c704f99245f00d5c3be0562d91931b4e7f1fb3fb36f41991eb76656ffbd6129f" + }, + "harness/fixtures/release1-native/semantic-ordered.ambiguous.txt": { + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + "harness/fixtures/release1-native/semantic-ordered.arcadia": { + "bytes": 4279, + "sha256": "c59ec1233d5a9fbb8e386f6d9b13f398be21baf813ba215efb51bfc1bb98db36" + }, + "harness/fixtures/release1-native/semantic-ordered.final-plan.json": { + "bytes": 17938, + "sha256": "30c5c3f614a74708763df612016483623792f4c65e2ed2fa662ac417cfb852eb" + }, + "harness/fixtures/release1-native/semantic-ordered.final-request.json": { + "bytes": 2098, + "sha256": "3b6c35ef78133f0980cb6fb53fbc29c5d3c4deb5dc4be01c2c3384e221987b9b" + }, + "harness/fixtures/release1-native/semantic-ordered.json": { + "bytes": 6996, + "sha256": "c159c60403abcf8fefab7f19cbad9989488e8ccc20b82c57597b22a0ae254802" + }, + "harness/fixtures/release1-native/semantic-ordered.manifest.json": { + "bytes": 1038, + "sha256": "af8a314a9b1d85a8f98affa5a6649f43d252851158798e1ad687bb1b6966b14f" + }, + "harness/fixtures/release1-native/semantic-ordered.steps.md": { + "bytes": 4549, + "sha256": "6004894e9d9f478b7fe1c3c3e0706dcc546d5df8557cff91b4cf26f8a4badf24" + }, + "harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json": { + "bytes": 1933, + "sha256": "71cca2a59f36b2a07d11dedbe9101226aa1a4a00d8ac244b5565202e56709f51" + }, + "harness/fixtures/release1-native/source5-recovery/generate.py": { + "bytes": 1496, + "sha256": "e44986c1d563499f25917441220b23883fc9cb0f89faf409873191f0212fbd2f" + }, + "harness/fixtures/release1-native/state-actions.arcadia": { + "bytes": 6224, + "sha256": "9cde82608110e56aa9e14c9e33eb38976f53aed74ab013145d411d5743a0ec26" + }, + "harness/fixtures/rest-coverage/README.md": { + "bytes": 1870, + "sha256": "df4a391312e0c8b20ccdcc850199443b08547be1b10b1f2447a067bf4bf544e6" + }, + "harness/fixtures/rest-coverage/coverage-probe.json": { + "bytes": 7951, + "sha256": "2d1ef7bdb5d7bf17b9fda33b1d1790d1d88289b3fd1e725f89b83a69771f9a43" + }, + "harness/fixtures/rest-coverage/run-coverage-probe.ps1": { + "bytes": 9381, + "sha256": "f24f19fca418b76a7249894a08c116e16b465a02f8f899d12b808ccfdd90a3bd" + }, + "harness/fixtures/run-timed-bundle-oracle.ps1": { + "bytes": 7466, + "sha256": "fab696486a574f9867c6e69a8a8b960e7e9722206441b9cc5f36d41f285f1a25" + }, + "harness/fixtures/scenario-authoring/README.md": { + "bytes": 14700, + "sha256": "6b651b180b76d10b00bc49d77a8f5e0b3ba0317446978f41b541f5bb23fedad8" + }, + "harness/fixtures/scenario-authoring/base/expected-baseline.json": { + "bytes": 38589, + "sha256": "abd8246c939ebb8a2162ef82e23d0fb322ee1a8b4d0d7c28a34037e42e86e841" + }, + "harness/fixtures/scenario-authoring/base/project.arcadia": { + "bytes": 19470, + "sha256": "9b1afbe96b0bf8fa613451b00929492f05a59b9eb2ca74c2c374a8b7bb7cba9e" + }, + "harness/fixtures/scenario-authoring/base/rationale.arcadia": { + "bytes": 86, + "sha256": "82df210f22f6aa336a3e7e00240977d671c7c3923db7bc58fb29e6d98b67ef49" + }, + "harness/fixtures/source7-native/README.md": { + "bytes": 7235, + "sha256": "7abccbc3da0980d1d099761d1a66d12c403a0f543954134537ecd4930d182b4b" + }, + "harness/fixtures/source7-native/base/expected-baseline.json": { + "bytes": 26428, + "sha256": "e2882a58f841f460f45d9d67db1606440e0d7997b9b29a784569ae48e4b23bfb" + }, + "harness/fixtures/source7-native/base/project.arcadia": { + "bytes": 13616, + "sha256": "5474d8ca05fd1375220dc0ad40d913631fde51159e9632bb9f290d1ecdab8dd7" + }, + "harness/fixtures/source7-native/base/rationale.arcadia": { + "bytes": 59, + "sha256": "e6a600e13036d4ad594a65380ed1b3d82d1772d583041bb0a624c9588a73047a" + }, + "harness/fixtures/source7-native/manifest.json": { + "bytes": 410, + "sha256": "6c57164a796234d83818d462d0a25f10e77062d405769e602ae2a7bec251e3b6" + }, + "harness/fixtures/staged-source-authoring/README.md": { + "bytes": 6876, + "sha256": "502be5e3bf9112447dc07b1a51fe0fa301754b47128a9b81877c185c6e797fee" + }, + "harness/fixtures/staged-source-authoring/base/notes.arcadia": { + "bytes": 63, + "sha256": "ad9de02963c20d1f43856455a797deb05ca674659e45f2c4b96b52783024f8bd" + }, + "harness/fixtures/staged-source-authoring/base/project.arcadia": { + "bytes": 593, + "sha256": "aa844950ec4fb86f262a257e1fc370a26557ea7978d8a4cab78265037660234c" + }, + "rust-toolchain.toml": { + "bytes": 66, + "sha256": "a6a0bbd29ffaa8182dc22d1d9149709f1091e47df40ed96eb8a78a711c66a4ce" + }, + "scripts/package-artifacts.py": { + "bytes": 18353, + "sha256": "d5aff2eb16c68885123f297b6438baf54ab7de1b1f9df847f87bd33e56031b2a" + }, + "scripts/package-common.ps1": { + "bytes": 5067, + "sha256": "22d60b19df0f6d5ed37b743f5175678f72f6a9b24658f8bcd90a20bb9d98a931" + }, + "scripts/package-linux.ps1": { + "bytes": 5655, + "sha256": "8fc915371f686921b6b33cc5ec7ce5d98ca5460916710b2f8a7ee28f5fb2411d" + }, + "scripts/package-readme.md": { + "bytes": 1902, + "sha256": "597cfb8da873e019d2658420f18ec059f0185fa9106ff9eafe934dfdd6ed266b" + }, + "scripts/package-windows.ps1": { + "bytes": 13094, + "sha256": "82d690683a76f1e9eb6b35eaecf97d59b0f781f9a71ae9aec1434b4a84298142" + }, + "scripts/test-package-artifacts.py": { + "bytes": 12218, + "sha256": "9212f51688a6d2e59c43b096262476e3f857666993dddabb2bf9a0cb7d92e25c" + }, + "scripts/test-windows-notices.ps1": { + "bytes": 16019, + "sha256": "120cf54a0b0bb3a2ffb42350f2bfdcfb8262a787eaac764424e01f20fa68c3f3" + }, + "scripts/windows-notices.ps1": { + "bytes": 16886, + "sha256": "25b051f92abc7759634268d893de51fca889a94f3751d4d756cb134d2b8076e5" + }, + "scripts/windows-notices/CC0-1.0.txt": { + "bytes": 7048, + "sha256": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499" + }, + "scripts/windows-notices/Cantarell-Regular-NOTICE.txt": { + "bytes": 1560, + "sha256": "2936eaab48b3b4ac2fa216a02f1bb0ccbbd8fe2227e9b930e526056f639def80" + }, + "scripts/windows-notices/DoumanAsh-clipboard-win-3b27cf2bfd1a-LICENSE": { + "bytes": 1338, + "sha256": "c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566" + }, + "scripts/windows-notices/Fira-OFL.txt": { + "bytes": 4479, + "sha256": "99bfb7c1e845b79d1031fcf37e678178eb4a8c7cc74a3f218ddf7223083a6fcd" + }, + "scripts/windows-notices/FiraSans-Regular-NOTICE.txt": { + "bytes": 337, + "sha256": "1e0baaa389b1647d06322ac9ea37822f31995408200ae5a2805a6b9bea0810d1" + }, + "scripts/windows-notices/GNOME-Cantarell-61362600e104-COPYING": { + "bytes": 4534, + "sha256": "7bb92a19f8f5485713f6e96a8331289b3aae49e8e44e7cad58b567e03be16d76" + }, + "scripts/windows-notices/Iced-Icons-NOTICE.txt": { + "bytes": 1483, + "sha256": "8175634e30f84ef72a24c4965fb0e87f45a3c1274cc0e79e0473af582cffd1c4" + }, + "scripts/windows-notices/aclysma-profiling-8271551172eb-LICENSE-APACHE": { + "bytes": 10818, + "sha256": "10d30a673cd5e9349bdc02aeb48f14b3386d27d0da32df8f0a555d4aa16aa551" + }, + "scripts/windows-notices/aclysma-profiling-8271551172eb-LICENSE-MIT": { + "bytes": 1093, + "sha256": "c8167fdeeed46d3f244d3f85c5bf998ce889343691c32be2c61a8bc4b5c08333" + }, + "scripts/windows-notices/brendanzab-gl-rs-ea503e8d5fb6-LICENSE": { + "bytes": 11358, + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + }, + "scripts/windows-notices/brendanzab-gl-rs-f150967b1c44-LICENSE": { + "bytes": 11358, + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + }, + "scripts/windows-notices/gfx-rs-rspirv-e91a5debc10c-LICENSE": { + "bytes": 11358, + "sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + }, + "scripts/windows-notices/hexf-parse-0.2.1-Cargo.toml": { + "bytes": 344, + "sha256": "4a56dbdbc12054fd33b7a2634a45cbbe6fe12f5e6f4081002cbfa527f7220c70" + }, + "scripts/windows-notices/iced-rs-iced-3997291f318a-LICENSE": { + "bytes": 1073, + "sha256": "fc9086ba4eba4b77e4c603906362742bc3715ffa2c7810c6074fe7780d3a0398" + }, + "scripts/windows-notices/iced-rs-iced-54abf81d13fe-LICENSE": { + "bytes": 1073, + "sha256": "fc9086ba4eba4b77e4c603906362742bc3715ffa2c7810c6074fe7780d3a0398" + }, + "scripts/windows-notices/llvm-ea7d852a70e8-libunwind-LICENSE.TXT": { + "bytes": 16706, + "sha256": "b5efebcaca80879234098e52d1725e6d9eb8fb96a19fce625d39184b705f7b6d" + }, + "scripts/windows-notices/manifest.json": { + "bytes": 18938, + "sha256": "ec0a106d22ab2985712abe44323bbaaebbc61aa37de37d73f7170087896c915d" + }, + "scripts/windows-notices/nical-lyon-56219c95c658-LICENSE-APACHE": { + "bytes": 554, + "sha256": "216486f29671a4262efe32af6d84a75bef398127f8c5f369b5c8305983887a06" + }, + "scripts/windows-notices/nical-lyon-56219c95c658-LICENSE-MIT": { + "bytes": 1080, + "sha256": "80f275e90d799911ed3830a7f242a2ef5a4ade2092fe0aa07bfb2d2cf2f2b95e" + }, + "scripts/windows-notices/nical-lyon-994526f2766c-LICENSE-APACHE": { + "bytes": 554, + "sha256": "216486f29671a4262efe32af6d84a75bef398127f8c5f369b5c8305983887a06" + }, + "scripts/windows-notices/nical-lyon-994526f2766c-LICENSE-MIT": { + "bytes": 1080, + "sha256": "80f275e90d799911ed3830a7f242a2ef5a4ade2092fe0aa07bfb2d2cf2f2b95e" + }, + "scripts/windows-notices/nical-rust_debug-93f414cd572b-LICENSE": { + "bytes": 1070, + "sha256": "7e98bed7f100747aaae7d82ab94fbe6b76fd6b84b09a3aaa413bff8196d0fbb1" + }, + "scripts/windows-notices/zakarumych-gpu-alloc-61020bdcb4eb-COPYING": { + "bytes": 347, + "sha256": "12121dfa588677007848ab922a45a1e3248868f6ff063598fceb3141d7897314" + }, + "scripts/windows-notices/zakarumych-gpu-alloc-7ddd1fe576fc-COPYING": { + "bytes": 347, + "sha256": "12121dfa588677007848ab922a45a1e3248868f6ff063598fceb3141d7897314" + }, + "scripts/windows-notices/zakarumych-gpu-descriptor-9f2fb9546591-COPYING": { + "bytes": 352, + "sha256": "f72143f7e9d9fb8bd336b139fa22f825a299591c29f2f7338c5566347b137613" + }, + "scripts/windows-notices/zakarumych-gpu-descriptor-bb28b4e55b74-COPYING": { + "bytes": 352, + "sha256": "f72143f7e9d9fb8bd336b139fa22f825a299591c29f2f7338c5566347b137613" + } +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/inputs.zip b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/inputs.zip new file mode 100644 index 00000000..d18e3d96 Binary files /dev/null and b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/inputs.zip differ diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/process.json b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/process.json new file mode 100644 index 00000000..fcbaa31b --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/process.json @@ -0,0 +1 @@ +{"pid": 772, "observed": "2026-09-23T09:44:19.087478+00:00", "command": ["cargo", "test", "-p", "oa-core", "--example", "state_multiselect_native", "--message-format=json", "--", "--test-threads=1"]} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/result.json b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/result.json new file mode 100644 index 00000000..4bb1b611 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/result.json @@ -0,0 +1,91 @@ +{ + "start": "2026-09-23T09:44:18.183228+00:00", + "command": [ + "cargo", + "test", + "-p", + "oa-core", + "--example", + "state_multiselect_native", + "--message-format=json", + "--", + "--test-threads=1" + ], + "cwd": "C:\\ar-multiselect-9e8r6t", + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "scope": "All Git-visible Cargo manifests/lock, crates (including patched dependency), build, .cargo, docs, examples, harness fixtures, compiled prototypes, and packaging scripts; all 71 tracked vendored files included", + "inputs": { + "bytes": 122682, + "sha256": "81318b52a4f2659f0687bfa459c7e3a24f39996dd5a8b6d7f21c3c78fa16c3b4" + }, + "archive": { + "bytes": 2947000, + "sha256": "cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e" + }, + "inputCount": 733, + "environment": { + "RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm", + "CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo", + "CARGO_TARGET_DIR": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm", + "CARGO_BUILD_JOBS": "1", + "CARGO_INCREMENTAL": "0" + }, + "helper": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + }, + "cargo": "cargo 1.98.1 (797e8a9bc 2026-08-05)", + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)", + "finish": "2026-09-23T09:44:55.179378+00:00", + "seconds": 36.09660960000474, + "exitCode": 0, + "inputsUnchanged": false, + "stdout": { + "bytes": 42721, + "sha256": "93c11a194e1c97abcdbce56c9b45be479bd1e60cfa5ecd873773246a696d146d" + }, + "stderr": { + "bytes": 320, + "sha256": "1b7e7597c4c20f47faf6695a257cc32bfce21b7fedcbf3a57cac6ea40114cd22" + }, + "artifacts": [ + { + "cargo": { + "reason": "compiler-artifact", + "package_id": "path+file:///C:/ar-multiselect-9e8r6t/crates/oa-core#0.2.0", + "manifest_path": "C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\Cargo.toml", + "target": { + "kind": [ + "example" + ], + "crate_types": [ + "bin" + ], + "name": "state_multiselect_native", + "src_path": "C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\examples\\state_multiselect_native.rs", + "edition": "2024", + "doc": false, + "doctest": false, + "test": false + }, + "profile": { + "opt_level": "0", + "debuginfo": 0, + "debug_assertions": true, + "overflow_checks": true, + "test": true + }, + "features": [], + "filenames": [ + "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\state_multiselect_native-d2e54d8aa041abf6.exe" + ], + "executable": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\state_multiselect_native-d2e54d8aa041abf6.exe", + "fresh": false + }, + "readback": { + "bytes": 25410048, + "sha256": "1c77465fc9c95e0a7079e23fa534c6f8e469b8b8b3a258fc8cdd47e02ff183c7" + } + } + ] +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/started.json b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/started.json new file mode 100644 index 00000000..9c1952c8 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/started.json @@ -0,0 +1,39 @@ +{ + "start": "2026-09-23T09:44:18.183228+00:00", + "command": [ + "cargo", + "test", + "-p", + "oa-core", + "--example", + "state_multiselect_native", + "--message-format=json", + "--", + "--test-threads=1" + ], + "cwd": "C:\\ar-multiselect-9e8r6t", + "head": "7e348d967d1787c052a145b7448636613a6a7054", + "scope": "All Git-visible Cargo manifests/lock, crates (including patched dependency), build, .cargo, docs, examples, harness fixtures, compiled prototypes, and packaging scripts; all 71 tracked vendored files included", + "inputs": { + "bytes": 122682, + "sha256": "81318b52a4f2659f0687bfa459c7e3a24f39996dd5a8b6d7f21c3c78fa16c3b4" + }, + "archive": { + "bytes": 2947000, + "sha256": "cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e" + }, + "inputCount": 733, + "environment": { + "RUSTUP_TOOLCHAIN": "stable-x86_64-pc-windows-gnullvm", + "CARGO_HOME": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo", + "CARGO_TARGET_DIR": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm", + "CARGO_BUILD_JOBS": "1", + "CARGO_INCREMENTAL": "0" + }, + "helper": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + }, + "cargo": "cargo 1.98.1 (797e8a9bc 2026-08-05)", + "rustc": "rustc 1.98.1 (48a229cea 2026-09-01)" +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/stderr.log b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/stderr.log new file mode 100644 index 00000000..dd2b0787 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/stderr.log @@ -0,0 +1,3 @@ + Compiling oa-core v0.2.0 (C:\ar-multiselect-9e8r6t\crates\oa-core) + Finished `test` profile [unoptimized] target(s) in 28.51s + Running unittests examples\state_multiselect_native.rs (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\target-gnullvm\debug\examples\state_multiselect_native-d2e54d8aa041abf6.exe) diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/stdout.log b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/stdout.log new file mode 100644 index 00000000..38c803c2 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/core-1/stdout.log @@ -0,0 +1,67 @@ +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\proc-macro2-1.0.107\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\proc-macro2-1.0.107\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\proc-macro2-8bd275bc14efcc6d\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107","linked_libs":[],"linked_paths":[],"cfgs":["wrap_proc_macro","proc_macro_span_location","proc_macro_span_file"],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\proc-macro2-678fa29727a3d951\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.24","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\unicode-ident-1.0.24\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"unicode_ident","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\unicode-ident-1.0.24\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libunicode_ident-37cacd323a49429c.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libunicode_ident-37cacd323a49429c.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\quote-1.0.47\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\quote-1.0.47\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\quote-78b7b00c15767e2b\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\quote-db749d68bbb2dbfe\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.107","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\proc-macro2-1.0.107\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"proc_macro2","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\proc-macro2-1.0.107\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libproc_macro2-80e9e45547beebf5.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libproc_macro2-80e9e45547beebf5.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.47","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\quote-1.0.47\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"quote","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\quote-1.0.47\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","proc-macro"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libquote-b79aec363b458000.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libquote-b79aec363b458000.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#autocfg@1.5.1","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\autocfg-1.5.1\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"autocfg","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\autocfg-1.5.1\\src\\lib.rs","edition":"2015","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libautocfg-58f70baf3de29c77.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libautocfg-58f70baf3de29c77.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.229","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_core-1.0.229\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_core-1.0.229\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["result","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\serde_core-161b3ae2683262f1\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.229","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\serde_core-a1da37301c02ee2a\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-traits-0.2.19\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-traits-0.2.19\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","i128","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\num-traits-34ffb4c64e9b5e22\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#syn@3.0.5","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\syn-3.0.5\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"syn","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\syn-3.0.5\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["clone-impls","default","derive","parsing","printing","proc-macro"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libsyn-2232b8c89352da95.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libsyn-2232b8c89352da95.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.3","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\getrandom-0.4.3\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\getrandom-0.4.3\\build.rs","edition":"2024","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\getrandom-168cb7f91c8645ca\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#windows-link@0.2.1","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\windows-link-0.2.1\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"windows_link","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\windows-link-0.2.1\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libwindows_link-23f115c823619f71.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libwindows_link-23f115c823619f71.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.61.2","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\windows-sys-0.61.2\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"windows_sys","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\windows-sys-0.61.2\\src\\lib.rs","edition":"2021","doc":true,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["Win32","Win32_Foundation","Win32_Storage","Win32_Storage_FileSystem","Win32_System","Win32_System_Console","Win32_System_SystemInformation","default"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libwindows_sys-43b3d9a205b229b7.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libwindows_sys-43b3d9a205b229b7.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.3","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\getrandom-4967b63cf7b288b8\\out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","linked_libs":[],"linked_paths":[],"cfgs":["has_total_cmp"],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\num-traits-418230edfe1b6b90\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_core@1.0.229","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_core-1.0.229\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_core","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_core-1.0.229\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["result","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde_core-f52ac0796dd4fc39.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde_core-f52ac0796dd4fc39.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.23","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\zmij-1.0.23\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\zmij-1.0.23\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\zmij-e8f67a20243f3063\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.4","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\cfg-if-1.0.4\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"cfg_if","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\cfg-if-1.0.4\\src\\lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libcfg_if-805b2ebc84d544f1.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libcfg_if-805b2ebc84d544f1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.4.3","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\getrandom-0.4.3\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"getrandom","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\getrandom-0.4.3\\src\\lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libgetrandom-6e8efb5a01ff0cbb.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libgetrandom-6e8efb5a01ff0cbb.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.23","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\zmij-c8f8f3187a72be14\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.19","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-traits-0.2.19\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_traits","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-traits-0.2.19\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","i128","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libnum_traits-49c57447e2766d62.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libnum_traits-49c57447e2766d62.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.20","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\thiserror-2.0.20\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\thiserror-2.0.20\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\thiserror-d9d1cecb0b9dfa9d\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.229","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde-1.0.229\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde-1.0.229\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","serde_derive","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\serde-c9911adbe87e85d2\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.151","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_json-1.0.151\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_json-1.0.151\\build.rs","edition":"2021","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\serde_json-28521d5277e5c15b\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.151","linked_libs":[],"linked_paths":[],"cfgs":["fast_arithmetic=\"64\""],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\serde_json-908788f6700df5fb\\out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.229","linked_libs":[],"linked_paths":[],"cfgs":["if_docsrs_then_no_serde_core"],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\serde-963672309bf9e4ba\\out"} +{"reason":"build-script-executed","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.20","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\thiserror-2e28320913ae6f8a\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.47","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-integer-0.1.47\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_integer","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-integer-0.1.47\\src\\lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","i128","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libnum_integer-cf43e8f5b615e1d9.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libnum_integer-cf43e8f5b615e1d9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#zmij@1.0.23","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\zmij-1.0.23\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"zmij","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\zmij-1.0.23\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libzmij-9f8770986f00ef15.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libzmij-9f8770986f00ef15.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.11","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\winapi-util-0.1.11\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"winapi_util","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\winapi-util-0.1.11\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libwinapi_util-be9ab0847edf5b05.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libwinapi_util-be9ab0847edf5b05.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@2.0.20","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\thiserror-impl-2.0.20\\Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"thiserror_impl","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\thiserror-impl-2.0.20\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\thiserror_impl-774eb7636a800724.dll","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libthiserror_impl-774eb7636a800724.dll.a"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.229","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_derive-1.0.229\\Cargo.toml","target":{"kind":["proc-macro"],"crate_types":["proc-macro"],"name":"serde_derive","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_derive-1.0.229\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\serde_derive-c07a6353fdd1d8a1.dll","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde_derive-c07a6353fdd1d8a1.dll.a"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.21.4","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\once_cell-1.21.4\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"once_cell","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\once_cell-1.21.4\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","race","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libonce_cell-f7e62c44737ecbb9.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libonce_cell-f7e62c44737ecbb9.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#sha1_smol@1.0.1","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\sha1_smol-1.0.1\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"sha1_smol","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\sha1_smol-1.0.1\\src\\lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libsha1_smol-c4b7e5ec8b41792e.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libsha1_smol-c4b7e5ec8b41792e.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#memchr@2.8.3","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\memchr-2.8.3\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"memchr","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\memchr-2.8.3\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libmemchr-088af8780d4df76d.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libmemchr-088af8780d4df76d.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.18","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\itoa-1.0.18\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"itoa","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\itoa-1.0.18\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libitoa-971c3e13d32ffcf2.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libitoa-971c3e13d32ffcf2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"path+file:///C:/ar-multiselect-9e8r6t/crates/oa-core#0.2.0","manifest_path":"C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\Cargo.toml","target":{"kind":["custom-build"],"crate_types":["bin"],"name":"build-script-build","src_path":"C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\build.rs","edition":"2024","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\oa-core-1fac039e071a74d1\\build-script-build.exe"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.5.0","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\fastrand-2.5.0\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"fastrand","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\fastrand-2.5.0\\src\\lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["alloc","default","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libfastrand-3df248e815d242e1.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libfastrand-3df248e815d242e1.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.27.0","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\tempfile-3.27.0\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"tempfile","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\tempfile-3.27.0\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","getrandom"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libtempfile-20c336358d7a22b5.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libtempfile-20c336358d7a22b5.rmeta"],"executable":null,"fresh":true} +{"reason":"build-script-executed","package_id":"path+file:///C:/ar-multiselect-9e8r6t/crates/oa-core#0.2.0","linked_libs":[],"linked_paths":[],"cfgs":[],"env":[],"out_dir":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\build\\oa-core-70aec2be9e005f38\\out"} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.151","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_json-1.0.151\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde_json","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde_json-1.0.151\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde_json-b48005beba7e4267.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde_json-b48005beba7e4267.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#uuid@1.26.0","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\uuid-1.26.0\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"uuid","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\uuid-1.26.0\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","rng","serde","sha1","std","v4","v5"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libuuid-f7851e8936dae115.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libuuid-f7851e8936dae115.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.229","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde-1.0.229\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"serde","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde-1.0.229\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","derive","serde_derive","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde-968c91fb77bb5d38.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libserde-968c91fb77bb5d38.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#thiserror@2.0.20","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\thiserror-2.0.20\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"thiserror","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\thiserror-2.0.20\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libthiserror-add096f46a35d6d2.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libthiserror-add096f46a35d6d2.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#same-file@1.0.6","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\same-file-1.0.6\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"same_file","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\same-file-1.0.6\\src\\lib.rs","edition":"2018","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libsame_file-a21d6395ce590b18.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libsame_file-a21d6395ce590b18.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"registry+https://github.com/rust-lang/crates.io-index#num-bigint@0.5.1","manifest_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-bigint-0.5.1\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"num_bigint","src_path":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\toolchain\\cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\num-bigint-0.5.1\\src\\lib.rs","edition":"2021","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":["default","std"],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libnum_bigint-7e87a41b738c784b.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\libnum_bigint-7e87a41b738c784b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"path+file:///C:/ar-multiselect-9e8r6t/crates/oa-core#0.2.0","manifest_path":"C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\Cargo.toml","target":{"kind":["lib"],"crate_types":["lib"],"name":"oa_core","src_path":"C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\src\\lib.rs","edition":"2024","doc":true,"doctest":true,"test":true},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":false},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\liboa_core-b1d9f219f5e0886b.rlib","C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\deps\\liboa_core-b1d9f219f5e0886b.rmeta"],"executable":null,"fresh":true} +{"reason":"compiler-artifact","package_id":"path+file:///C:/ar-multiselect-9e8r6t/crates/oa-core#0.2.0","manifest_path":"C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\Cargo.toml","target":{"kind":["example"],"crate_types":["bin"],"name":"state_multiselect_native","src_path":"C:\\ar-multiselect-9e8r6t\\crates\\oa-core\\examples\\state_multiselect_native.rs","edition":"2024","doc":false,"doctest":false,"test":false},"profile":{"opt_level":"0","debuginfo":0,"debug_assertions":true,"overflow_checks":true,"test":true},"features":[],"filenames":["C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\state_multiselect_native-d2e54d8aa041abf6.exe"],"executable":"C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\target-gnullvm\\debug\\examples\\state_multiselect_native-d2e54d8aa041abf6.exe","fresh":false} +{"reason":"build-finished","success":true} + +running 11 tests +test tests::generated_json_and_real_source_bundle_are_accepted_unchanged ... ok +test tests::hide_must_preserve_child_routes_and_semantic_objects ... ok +test tests::internal_pins_left_behind_and_external_pins_moved_are_rejected ... ok +test tests::manual_attachments_and_label_data_must_remain_exact ... ok +test tests::moved_and_hidden_source_inputs_are_checked_against_complete_expected_projects ... ok +test tests::omitted_and_double_moved_child_are_rejected ... ok +test tests::real_group_command_outputs_pass_and_are_not_unchanged ... ok +test tests::real_hide_command_outputs_pass_before_and_after_movement ... ok +test tests::semantic_other_view_fixed_node_and_trace_drift_are_rejected ... ok +test tests::unchanged_rejects_valid_semantic_drift_without_geometry_changes ... ok +test tests::zero_and_excessive_moves_cannot_satisfy_move_evidence ... ok + +test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.25s + diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/files.json b/harness/experiments/state-multiselect-20260923/incidents/core-1/files.json new file mode 100644 index 00000000..c45cfc46 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/files.json @@ -0,0 +1,50 @@ +{ + "README.md": { + "bytes": 1113, + "sha256": "23b6099c058997f1ed12c20b0dd89d124d2e6915eccf02bdbe1068544256a748" + }, + "core-1/inputs.json": { + "bytes": 122682, + "sha256": "81318b52a4f2659f0687bfa459c7e3a24f39996dd5a8b6d7f21c3c78fa16c3b4" + }, + "core-1/inputs.zip": { + "bytes": 2947000, + "sha256": "cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e" + }, + "core-1/process.json": { + "bytes": 199, + "sha256": "c59cb33a7a7339a9b876b87e377ef57a8840bf76a411dc80d6cf5e32d365a9f8" + }, + "core-1/result.json": { + "bytes": 3196, + "sha256": "ca61228c182b4824492a6248e2c37dc4519e35b5b077212e72a4283bf3b0ffab" + }, + "core-1/started.json": { + "bytes": 1403, + "sha256": "2aa37908fb7eb8f49819fe8e96c59e6254f560b5c7058f7491c6c38d4b304487" + }, + "core-1/stderr.log": { + "bytes": 320, + "sha256": "1b7e7597c4c20f47faf6695a257cc32bfce21b7fedcbf3a57cac6ea40114cd22" + }, + "core-1/stdout.log": { + "bytes": 42721, + "sha256": "93c11a194e1c97abcdbce56c9b45be479bd1e60cfa5ecd873773246a696d146d" + }, + "gate-helper.py": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + }, + "identity.json": { + "bytes": 1455, + "sha256": "b065e20e1130fbd1caff052e71e01da083c14eaf785748b3d32c3a691f7146a9" + }, + "prepare.py": { + "bytes": 1866, + "sha256": "0550fb880635e27a193d2b5166f1dbbe1edb2091f3bf5d23be5fc3fa3b4ae63b" + }, + "verify.py": { + "bytes": 3726, + "sha256": "3a33fb46f3b9b8a30a35dbc6469d404cca94fa2ed40804101719f2d2e686d6d0" + } +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/gate-helper.py b/harness/experiments/state-multiselect-20260923/incidents/core-1/gate-helper.py new file mode 100644 index 00000000..ef3903d3 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/gate-helper.py @@ -0,0 +1,48 @@ +from pathlib import Path +import datetime, hashlib, json, os, subprocess, sys, time, zipfile + +# Process-local disk recovery: do not write another Rust incremental cache. +os.environ['CARGO_INCREMENTAL'] = '0' + +root = Path('C:/ar-multiselect-9e8r6t') +out = Path(__file__).resolve().parent / sys.argv[1] +out.mkdir(parents=True, exist_ok=False) +def now(): return datetime.datetime.now(datetime.timezone.utc).isoformat() +def digest(path): + data = path.read_bytes() + return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()} +def inputs(): + paths = subprocess.check_output(['git','ls-files','--cached','--others','--exclude-standard','-z'],cwd=root).decode().split('\0') + return {p:digest(root/p) for p in sorted(set(paths)) if p and (p in ('Cargo.toml','Cargo.lock','rust-toolchain.toml') or p.startswith(('crates/','build/','.cargo/','scripts/','docs/','examples/','harness/fixtures/','harness/prototypes/')))} +before=inputs() +assert len([p for p in before if p.startswith('crates/vendor/iced_widget/')]) == 71, 'Vendored input inventory changed' +assert all(before[p] == digest(root/p) for p in before) +(out/'inputs.json').write_text(json.dumps(before,indent=2),encoding='utf-8') +with zipfile.ZipFile(out/'inputs.zip','x',compression=zipfile.ZIP_DEFLATED) as z: + for p in before: z.write(root/p,p) +command=['cargo',*sys.argv[2:]] +start={'start':now(),'command':command,'cwd':str(root),'head':subprocess.check_output(['git','rev-parse','HEAD'],cwd=root,text=True).strip(), + 'scope':'All Git-visible Cargo manifests/lock, crates (including patched dependency), build, .cargo, docs, examples, harness fixtures, compiled prototypes, and packaging scripts; all 71 tracked vendored files included', + 'inputs':digest(out/'inputs.json'),'archive':digest(out/'inputs.zip'),'inputCount':len(before), + 'environment':{k:os.environ.get(k) for k in ('RUSTUP_TOOLCHAIN','CARGO_HOME','CARGO_TARGET_DIR','CARGO_BUILD_JOBS','CARGO_INCREMENTAL')}, + 'helper':digest(Path(__file__).resolve()), + 'cargo':subprocess.check_output(['cargo','--version'],text=True).strip(),'rustc':subprocess.check_output(['rustc','--version'],text=True).strip()} +(out/'started.json').write_text(json.dumps(start,indent=2),encoding='utf-8') +t=time.perf_counter() +with (out/'stdout.log').open('xb') as stdout,(out/'stderr.log').open('xb') as stderr: + r=subprocess.Popen(command,cwd=root,stdout=stdout,stderr=stderr) + (out/'process.json').write_text(json.dumps({'pid':r.pid,'observed':now(),'command':command}),encoding='utf-8') + r.wait() +artifacts=[] +for line in (out/'stdout.log').read_text(encoding='utf-8',errors='replace').splitlines(): + try: item=json.loads(line) + except json.JSONDecodeError: continue + if isinstance(item,dict) and item.get('reason')=='compiler-artifact' and item.get('executable'): + path=Path(item['executable']) + artifacts.append({'cargo':item,'readback':digest(path) if path.exists() else None}) +result={**start,'finish':now(),'seconds':time.perf_counter()-t,'exitCode':r.returncode,'inputsUnchanged':before==inputs(), + 'stdout':digest(out/'stdout.log'),'stderr':digest(out/'stderr.log'),'artifacts':artifacts} +(out/'result.json').write_text(json.dumps(result,indent=2),encoding='utf-8') +print(json.dumps({k:v for k,v in result.items() if k!='artifacts'},indent=2)) +print('Executable artifacts:',len(artifacts)) +sys.exit(0 if r.returncode==0 and result['inputsUnchanged'] else 1) diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/identity.json b/harness/experiments/state-multiselect-20260923/incidents/core-1/identity.json new file mode 100644 index 00000000..2512c835 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/identity.json @@ -0,0 +1,48 @@ +{ + "raw": { + "inputs.json": { + "bytes": 122682, + "sha256": "81318b52a4f2659f0687bfa459c7e3a24f39996dd5a8b6d7f21c3c78fa16c3b4" + }, + "inputs.zip": { + "bytes": 2947000, + "sha256": "cecd6ae0ba45add8e913d9913cd4960c01928db0a4bc795902b7954ffb6f692e" + }, + "process.json": { + "bytes": 199, + "sha256": "c59cb33a7a7339a9b876b87e377ef57a8840bf76a411dc80d6cf5e32d365a9f8" + }, + "result.json": { + "bytes": 3196, + "sha256": "ca61228c182b4824492a6248e2c37dc4519e35b5b077212e72a4283bf3b0ffab" + }, + "started.json": { + "bytes": 1403, + "sha256": "2aa37908fb7eb8f49819fe8e96c59e6254f560b5c7058f7491c6c38d4b304487" + }, + "stderr.log": { + "bytes": 320, + "sha256": "1b7e7597c4c20f47faf6695a257cc32bfce21b7fedcbf3a57cac6ea40114cd22" + }, + "stdout.log": { + "bytes": 42721, + "sha256": "93c11a194e1c97abcdbce56c9b45be479bd1e60cfa5ecd873773246a696d146d" + } + }, + "helper": { + "bytes": 3502, + "sha256": "6b8fcf64a1eb0869ae29abd66b4e89564a1813fef5898b54f2a17a3230dbaa06" + }, + "mismatches": { + "crates/oa-desktop/src/model_menu.rs": { + "map": { + "bytes": 44572, + "sha256": "3844be8ec15fe2c5614ff5625ec7b63234fbf5a6d126de5a85c8f5c51dfa7101" + }, + "archive": { + "bytes": 44757, + "sha256": "82998c97e8d706b1ce1c1fb7058eaeabfceae3004e38f60d6076194968dc5272" + } + } + } +} diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/prepare.py b/harness/experiments/state-multiselect-20260923/incidents/core-1/prepare.py new file mode 100644 index 00000000..3a1bd3a0 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/prepare.py @@ -0,0 +1,32 @@ +"""Preserve the raw core-1 capture incident separately from strict gate packages.""" +from pathlib import Path +import argparse, json +from verify import HERE, RAW, need, pin, parsed, verify + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root',type=Path,required=True) + parser.add_argument('--helper',type=Path,action='append',default=[]) + parser.add_argument('--output',type=Path) + parser.add_argument('--check-only',action='store_true') + args=parser.parse_args() + gates=args.root/'.local/state-group-gates'; folder=gates/'core-1' + need(all(p.is_file() and not p.is_symlink() for p in folder.iterdir()) and {p.name for p in folder.iterdir()}==RAW,'exact closed raw inventory') + raw={n:(folder/n).read_bytes() for n in RAW} + identity=parsed((HERE/'identity.json').read_bytes()) + matches=[p.read_bytes() for p in list(gates.glob('*.py'))+args.helper if pin(p.read_bytes())==identity['helper']] + need(bool(matches),'exact historical helper required') + summary=verify(raw,matches[0],identity) + if args.check_only: + print('PASS incident check-only, no package written:',summary); return + need(args.output is not None,'explicit --output required') + out=args.output.resolve(); need(not out.exists(),'existing output refused') + out.mkdir(parents=True); (out/'core-1').mkdir() + payload={'core-1/'+n:b for n,b in raw.items()} + payload['gate-helper.py']=matches[0] + for n in ('prepare.py','verify.py','README.md','identity.json'): payload[n]=(HERE/n).read_bytes() + for n,b in payload.items(): + with (out/n).open('xb') as f: f.write(b) + with (out/'files.json').open('x',encoding='utf-8') as f: json.dump({n:pin(b) for n,b in sorted(payload.items())},f,indent=2) + print('Preserved incident, not a gate pass:',out,summary) +if __name__=='__main__': main() diff --git a/harness/experiments/state-multiselect-20260923/incidents/core-1/verify.py b/harness/experiments/state-multiselect-20260923/incidents/core-1/verify.py new file mode 100644 index 00000000..b7e34c42 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/incidents/core-1/verify.py @@ -0,0 +1,53 @@ +"""Verify one pinned capture incident; never count it as a normal gate pass.""" +from pathlib import Path +import argparse, hashlib, io, json, stat, zipfile +HERE = Path(__file__).resolve().parent +RAW = {'started.json','process.json','result.json','stdout.log','stderr.log','inputs.json','inputs.zip'} +COMMAND = ['cargo','test','-p','oa-core','--example','state_multiselect_native','--message-format=json','--','--test-threads=1'] +CHANGED = 'crates/oa-desktop/src/model_menu.rs' +def need(ok, message): + if not ok: raise ValueError(message) +def pin(data): return {'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()} +def parsed(data): + def unique(items): + out={} + for k,v in items: + need(k not in out, 'duplicate JSON key') + out[k]=v + return out + return json.loads(data, object_pairs_hook=unique) +def verify(raw, helper, identity): + need(set(raw)==set(identity['raw'])==RAW, 'exact seven raw files') + need({n:pin(b) for n,b in raw.items()}==identity['raw'], 'exact incident raw pins') + start,process,result=[parsed(raw[n+'.json']) for n in ('started','process','result')] + need(all(result.get(k)==v for k,v in start.items()), 'start/result relationship') + need(result['command']==process['command']==COMMAND, 'specific core example test command') + need(result['exitCode']==0 and result['inputsUnchanged'] is False, 'incident status, not accepted gate') + need(pin(helper)==identity['helper']==result['helper'], 'historical helper pin') + for field,name in [('inputs','inputs.json'),('archive','inputs.zip'),('stdout','stdout.log'),('stderr','stderr.log')]: + need(result[field]==pin(raw[name]), field+' pin') + inputs=parsed(raw['inputs.json']) + need(len(inputs)==result['inputCount']==733, 'historical input count') + actual={} + with zipfile.ZipFile(io.BytesIO(raw['inputs.zip'])) as z: + need(len(z.namelist())==len(set(z.namelist())) and set(z.namelist())==set(inputs), 'exact source inventory') + for item in z.infolist(): + name=item.filename + need(not item.is_dir() and not stat.S_ISLNK(item.external_attr>>16) and '\\' not in name and ':' not in name and not name.startswith('/') and all(p not in ('','.','..') for p in name.split('/')), 'safe source name') + actual[name]=pin(z.read(name)) + mismatch={n:{'map':inputs[n],'archive':actual[n]} for n in inputs if inputs[n]!=actual[n]} + need(set(mismatch)=={CHANGED} and mismatch==identity['mismatches'], 'exact sole known map/archive mismatch') + need(mismatch[CHANGED]['map']['bytes']==44572 and mismatch[CHANGED]['archive']['bytes']==44757, 'known mismatch sizes') + need(b'test result: ok. 11 passed; 0 failed;' in raw['stdout.log'], 'recorded eleven-test result') + return {'incident':'core-1','acceptedGate':False,'rawFiles':7,'mismatchedInputs':list(mismatch)} +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('package',nargs='?',type=Path,default=HERE) + args=parser.parse_args(); root=args.package + files=parsed((root/'files.json').read_bytes()) + expected={'prepare.py','verify.py','README.md','identity.json','gate-helper.py'}|{'core-1/'+n for n in RAW} + need(set(files)==expected, 'package file inventory') + need({p.relative_to(root).as_posix() for p in root.rglob('*') if p.is_file()}==expected|{'files.json'}, 'exact on-disk package files') + for n,p in files.items(): need(pin((root/n).read_bytes())==p, 'package pin '+n) + print('PASS incident consistency; not a gate pass:', verify({n:(root/'core-1'/n).read_bytes() for n in RAW},(root/'gate-helper.py').read_bytes(),parsed((root/'identity.json').read_bytes()))) +if __name__=='__main__': main() diff --git a/harness/experiments/state-multiselect-20260923/native/README.md b/harness/experiments/state-multiselect-20260923/native/README.md new file mode 100644 index 00000000..4b55a2c8 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/native/README.md @@ -0,0 +1,27 @@ +# State multiselect native evidence tools + +Candidate tools; no native run or acceptance is asserted by their presence. +Prepare only a closed `.local/state-multiselect-native/run-1`: + +``` +python -B prepare.py --root CHECKOUT --runs run-1 --check-only +``` + +Declare any additional unchanged snapshot labels with `--extra-unchanged LABEL ...`. +Nine original history snapshots remain mandatory. Each additional snapshot must +pass every checker/provenance/time/source check and equal baseline bytes exactly. +All three distinct history byte groups, complete-Project comparisons, input-map +and executable relationships, unchanged flags, captured-window relationships, +and exact archive inventories remain required. Source count must match the +fixture and history record. Only `views/diagram_state_view.arcadia` may change. + +After completed native observation and final source verification, omit +`--check-only` and provide +`--output CHECKOUT/harness/experiments/state-multiselect-20260923/native`. +Run `python -B verify.py PACKAGE` to check a written package. Existing outputs +are refused. Helpers are preserved as pinned data; they are never executed. +Referenced full build capsules/logs belong in the sibling checks package. + +Offline byte/receipt consistency does not authenticate actions, establish +usability, rerun the checker, or prove native acceptance. Native action +observations and limitations require a separate reviewed record. diff --git a/harness/experiments/state-multiselect-20260923/native/evidence.zip b/harness/experiments/state-multiselect-20260923/native/evidence.zip new file mode 100644 index 00000000..e1843b57 Binary files /dev/null and b/harness/experiments/state-multiselect-20260923/native/evidence.zip differ diff --git a/harness/experiments/state-multiselect-20260923/native/files.json b/harness/experiments/state-multiselect-20260923/native/files.json new file mode 100644 index 00000000..5a711d06 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/native/files.json @@ -0,0 +1,22 @@ +{ + "prepare.py": { + "bytes": 7494, + "sha256": "0ba52dc39d08888e34d2b84e25f40acbcfda97aa74c2fd21319967867e0008ae" + }, + "verify.py": { + "bytes": 15255, + "sha256": "26e08398561c1753926cb994d674d311307970f06f0f8ae68ca6dec4f5e9a8e4" + }, + "README.md": { + "bytes": 1457, + "sha256": "dc13ee4603368a86b49cd54d5c3b19c7759e81a16ccc3c782882285db0758d26" + }, + "manifest.json": { + "bytes": 42972, + "sha256": "088fd71a7e41536287aeeed32ab6da41130a0f3b3621dd752b88290a435bff50" + }, + "evidence.zip": { + "bytes": 3384779, + "sha256": "1d08c1bc6af4856a569ddd320ddf5403a302b8d9437750663a498541e489d663" + } +} diff --git a/harness/experiments/state-multiselect-20260923/native/manifest.json b/harness/experiments/state-multiselect-20260923/native/manifest.json new file mode 100644 index 00000000..8eb05c3b --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/native/manifest.json @@ -0,0 +1,975 @@ +{ + "schema": 1, + "kind": "multiselect-native", + "runs": { + "run-1": { + "originalRun": "C:/ar-multiselect-9e8r6t/.local/state-multiselect-native/run-1", + "originalFixture": "C:/ar-multiselect-9e8r6t/.local/state-multiselect-native/fixture-1", + "helpers": { + "helper": "helpers/68c17b874f25c2b65a0aa81b6db6de9a28184388fee04185e47615b022cfbdf3.py", + "capture": "helpers/c11446a694a6357c77e2cba405c5a078b29eef5cdd2342579d234b4e20270eb1.js", + "snapshotHelper": "helpers/4301a93f23db7d43399ed87c6eede2196d38911aba09a391fae5bcd4b691fa7b.py", + "history": "helpers/dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723.py" + }, + "builds": { + "gui": { + "receipt": "builds/172faec4d7708e4539468c3a9062d424f0e00cb80e827cd579274942c757eccb/result.json", + "inputs": "builds/172faec4d7708e4539468c3a9062d424f0e00cb80e827cd579274942c757eccb/inputs.json", + "originalRelative": ".local/state-group-gates/windows-build-1/result.json", + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + }, + "checker": { + "receipt": "builds/03c22f6269b023f77d0fb3e713d5bbab8d14cf6e67ab4dfc613b4691fe4a6955/result.json", + "inputs": "builds/03c22f6269b023f77d0fb3e713d5bbab8d14cf6e67ab4dfc613b4691fe4a6955/inputs.json", + "originalRelative": ".local/state-group-gates/native-build-1/result.json", + "helper": "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py" + } + }, + "extraUnchangedSnapshots": [ + "row-mixed-unchanged" + ], + "checkerSource": { + "inputPath": "crates/oa-core/examples/state_multiselect_native.rs", + "member": "checker-source/5db7a00b044c27b851731b3d8cb16cc3f638a5242bf53bd46b3a5e1df4de0460.rs" + } + } + }, + "members": { + "builds/03c22f6269b023f77d0fb3e713d5bbab8d14cf6e67ab4dfc613b4691fe4a6955/inputs.json": { + "bytes": 123053, + "sha256": "4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf" + }, + "builds/03c22f6269b023f77d0fb3e713d5bbab8d14cf6e67ab4dfc613b4691fe4a6955/result.json": { + "bytes": 3311, + "sha256": "03c22f6269b023f77d0fb3e713d5bbab8d14cf6e67ab4dfc613b4691fe4a6955" + }, + "builds/172faec4d7708e4539468c3a9062d424f0e00cb80e827cd579274942c757eccb/inputs.json": { + "bytes": 123053, + "sha256": "4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf" + }, + "builds/172faec4d7708e4539468c3a9062d424f0e00cb80e827cd579274942c757eccb/result.json": { + "bytes": 3193, + "sha256": "172faec4d7708e4539468c3a9062d424f0e00cb80e827cd579274942c757eccb" + }, + "checker-source/5db7a00b044c27b851731b3d8cb16cc3f638a5242bf53bd46b3a5e1df4de0460.rs": { + "bytes": 31343, + "sha256": "5db7a00b044c27b851731b3d8cb16cc3f638a5242bf53bd46b3a5e1df4de0460" + }, + "helpers/1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20.py": { + "bytes": 3827, + "sha256": "1368375b1fba0dfc50b77ea4f406dba71960a78317ce8f259d4c4a0254863a20" + }, + "helpers/4301a93f23db7d43399ed87c6eede2196d38911aba09a391fae5bcd4b691fa7b.py": { + "bytes": 2503, + "sha256": "4301a93f23db7d43399ed87c6eede2196d38911aba09a391fae5bcd4b691fa7b" + }, + "helpers/68c17b874f25c2b65a0aa81b6db6de9a28184388fee04185e47615b022cfbdf3.py": { + "bytes": 3411, + "sha256": "68c17b874f25c2b65a0aa81b6db6de9a28184388fee04185e47615b022cfbdf3" + }, + "helpers/c11446a694a6357c77e2cba405c5a078b29eef5cdd2342579d234b4e20270eb1.js": { + "bytes": 1405, + "sha256": "c11446a694a6357c77e2cba405c5a078b29eef5cdd2342579d234b4e20270eb1" + }, + "helpers/dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723.py": { + "bytes": 2192, + "sha256": "dc3ad2c3491acda3290099e4c3af0ef6d5c2362ca1df9c2ee3b8f113b2f5e723" + }, + "runs/run-1/fixture/baseline.json": { + "bytes": 18490, + "sha256": "07432f30fecbf0710ab3c01dbfa760df18e781ac81713ed86fa2ea232eff0d07" + }, + "runs/run-1/fixture/manifest.json": { + "bytes": 2897, + "sha256": "2d4a067b45181b9b66b84598464b20045f13ce23890d3a7c5b195e2f7f9f7b64" + }, + "runs/run-1/fixture/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/fixture/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/fixture/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/fixture/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/fixture/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/fixture/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/fixture/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/fixture/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/fixture/source/views/diagram_state_view.arcadia": { + "bytes": 3781, + "sha256": "feda42f7f4329b925b7f87289b384f075705616a09a310a99c96e4177f6fee85" + }, + "runs/run-1/raw/01-start-0.jpg": { + "bytes": 116250, + "sha256": "dcaf44d81bf7d9bccabe1aab8e6d1143e29c3561b56bda56da5fbcfe5ce7094f" + }, + "runs/run-1/raw/01-start.json": { + "bytes": 919, + "sha256": "5422b74fa139899ab9f355c787aed483bd7a8e40a8796aa291e3bf8a884947d3" + }, + "runs/run-1/raw/02-open-dialog-0.jpg": { + "bytes": 114527, + "sha256": "a2622f57aa5e83353244216c177110d7627605cbef02cfde3ea4ceb5d543d72c" + }, + "runs/run-1/raw/02-open-dialog.json": { + "bytes": 925, + "sha256": "300dfa12f3c5f170130bc4b2a6ef0eb1d75ecddc7497f3b325e174165d6e389d" + }, + "runs/run-1/raw/03-project-open-0.jpg": { + "bytes": 133077, + "sha256": "d08a5d7a3c65b079de4d83af523d8398732e976fc4cd7c1672e5e1231ee8fd36" + }, + "runs/run-1/raw/03-project-open.json": { + "bytes": 934, + "sha256": "1cf0f6b55cfc904c0e023e463d844aa3929c6728da6b826a8c1b3a87ccf371c9" + }, + "runs/run-1/raw/04-baseline-saved-0.jpg": { + "bytes": 131419, + "sha256": "64e2fdea1f0372024bf377493416f75fce34f3ae6d6cdf1870b40c8ab7ae0e0b" + }, + "runs/run-1/raw/04-baseline-saved.json": { + "bytes": 938, + "sha256": "2cd7ccc03073bda0c2468437dcd90cf00e06354cac8cafba0cfd73aae944d640" + }, + "runs/run-1/raw/05-a-selected-0.jpg": { + "bytes": 146454, + "sha256": "a1b3da460a3e3aeaf611321bc2436dd0551463dce0b03a28ef60d509daec671c" + }, + "runs/run-1/raw/05-a-selected.json": { + "bytes": 934, + "sha256": "74d3eda5e9b8512e8a842cc302183ac97b64cdcf46602519dfa2ac624542b3db" + }, + "runs/run-1/raw/06-row-context-0.jpg": { + "bytes": 148643, + "sha256": "97c37986756b9a64d3995ea824b5d6c5124345bda0c309e1da09f7aaf1554491" + }, + "runs/run-1/raw/06-row-context.json": { + "bytes": 935, + "sha256": "6edea69009268837c510666a7697700a081fd8685c12aef0b34485965225a4a7" + }, + "runs/run-1/raw/07-row-added-0.jpg": { + "bytes": 145885, + "sha256": "ea7a830f2ed4b35b4173dc828de2184e65162c39c73b19cc1ea0c90c5cfa0ab4" + }, + "runs/run-1/raw/07-row-added.json": { + "bytes": 933, + "sha256": "d087cebd389786ba05a6dc355410fa371f7e369b692d93a6805684091b7fb11e" + }, + "runs/run-1/raw/08-mixed-drag-refused-0.jpg": { + "bytes": 151098, + "sha256": "3ad911c0f3a44d3078411570bb87ddecb7d5e27661b1d0421275210290668201" + }, + "runs/run-1/raw/08-mixed-drag-refused.json": { + "bytes": 942, + "sha256": "eb4c94795c8ed43e492e9748ba2fb95d8f56020e91d9cd93ccafd777c8f61331" + }, + "runs/run-1/raw/09-mixed-refusal-saved-0.jpg": { + "bytes": 148444, + "sha256": "d268d68462744bc54cdc2f333ce1d119178d17b6746dc5aede2cae226fe2f89a" + }, + "runs/run-1/raw/09-mixed-refusal-saved.json": { + "bytes": 943, + "sha256": "1b971ab580bd8a27c74ae29f71a9c6a94022fb025b6c739923954a64efaa014b" + }, + "runs/run-1/raw/10-stationary-collapse-0.jpg": { + "bytes": 146515, + "sha256": "430f5c79dd891d88c4a8e25f02d644d9e23e709bbdd9229c1668e486a65dffad" + }, + "runs/run-1/raw/10-stationary-collapse.json": { + "bytes": 943, + "sha256": "037cc2690a0334b7f0032e4fdffe9e81793267ca47184d0b78fc10a2d13c1c7e" + }, + "runs/run-1/raw/11-b-context-0.jpg": { + "bytes": 155065, + "sha256": "4321ffcff5b002ada96714d7e5d8c6ac55db76e1fdd539ab3f3a053ac9b43ac6" + }, + "runs/run-1/raw/11-b-context.json": { + "bytes": 933, + "sha256": "f9cdd902dda8e296a733e2d8728b22f3908da6be97d7e0c3c297bb05ec7c529b" + }, + "runs/run-1/raw/12-placement-group-0.jpg": { + "bytes": 147223, + "sha256": "ffdd50679ebf1f4ef581e943e4c8edd4e0170d721a38e1b2e4f9090c570d58a4" + }, + "runs/run-1/raw/12-placement-group.json": { + "bytes": 939, + "sha256": "cf462a9354aa79af5e0975162d530bb4adf1e89f7d52492b62ae7da7034928b1" + }, + "runs/run-1/raw/13-group-moved-0.jpg": { + "bytes": 147935, + "sha256": "fa65ffefd695b4b5aef67c22bf23148906182c769e0e512e98a1f7c32435035f" + }, + "runs/run-1/raw/13-group-moved.json": { + "bytes": 943, + "sha256": "9261f3bb668dd6660c2a0355adb982a0f00f6961159d8706bdc1c982a0b7a816" + }, + "runs/run-1/raw/14-move-saved-0.jpg": { + "bytes": 146770, + "sha256": "f055b46341c302063cb702dc6619f2509f5c4a80adc5eb6bcf48d77098c0026f" + }, + "runs/run-1/raw/14-move-saved.json": { + "bytes": 934, + "sha256": "306876d9b6de633d623227a16a84f9fcc066b37008c436e670bfd3df9d816cc8" + }, + "runs/run-1/raw/15-move-undo-0.jpg": { + "bytes": 148428, + "sha256": "268fdaaa4d4832cf3bdcbd69d6ac062ece52301b7cd0ae38cd1edd38fa157fe3" + }, + "runs/run-1/raw/15-move-undo.json": { + "bytes": 943, + "sha256": "300a234ed16f53fce88f7f8eaf9d252af8e98122f5c1f8aa13f6dbad8d56012c" + }, + "runs/run-1/raw/16-move-undo-saved-0.jpg": { + "bytes": 148316, + "sha256": "c4e986553c2938bffca8ee1f7e6ee7c4a3aa077e6ff6bd9da50d35e10684a9f3" + }, + "runs/run-1/raw/16-move-undo-saved.json": { + "bytes": 941, + "sha256": "fef07b40f46a6138623de1f10d5e74c81f87f18a35059b132fe2af192e6d17dc" + }, + "runs/run-1/raw/17-move-redo-0.jpg": { + "bytes": 146795, + "sha256": "37476c5a02d9ca0db214e244040b112dc50fd4d1418ec3af96f0a4f4776e4dde" + }, + "runs/run-1/raw/17-move-redo.json": { + "bytes": 943, + "sha256": "751ccfe6dc9acecdc0e7225aedd17be44ea374f368cba5ab2191c8c659a55af2" + }, + "runs/run-1/raw/18-move-redo-saved-0.jpg": { + "bytes": 146823, + "sha256": "c248b70901e52881bce338b6fe0f7fdcaf19df8fad349f24b2c64be54a187069" + }, + "runs/run-1/raw/18-move-redo-saved.json": { + "bytes": 939, + "sha256": "cfb609930813f6b8cd051c7daf538f8feb04be374be28c65fbd498dd937d82d9" + }, + "runs/run-1/raw/19-before-hide-undo-0.jpg": { + "bytes": 148446, + "sha256": "a2870258332cc555c9b8abfc3db070ff1582ea449ed6d1d7eedd19e977ea8021" + }, + "runs/run-1/raw/19-before-hide-undo.json": { + "bytes": 950, + "sha256": "4336190711b265ab5bf508a48528fd0e4b448b48cca86d9907d4704a05087de7" + }, + "runs/run-1/raw/20-before-hide-saved-0.jpg": { + "bytes": 148362, + "sha256": "6cf90eb40ba2d70dd66da6fd5eb4ef07a787a11a05841d5fd2abe04630b34e02" + }, + "runs/run-1/raw/20-before-hide-saved.json": { + "bytes": 943, + "sha256": "1c74bc7db156e7aa9a79dea803b77d991a5ae242fbfc82884fcea2235714d6d7" + }, + "runs/run-1/raw/21-group-menu-0.jpg": { + "bytes": 153978, + "sha256": "472f92a8a5aaa9ca983e5ad9262414e3abe82b0618996206fdd28199d543bcdd" + }, + "runs/run-1/raw/21-group-menu.json": { + "bytes": 934, + "sha256": "cfbba47a72d05700bd6c159175250bb3dcb7a4277527df4c0873853fb87661df" + }, + "runs/run-1/raw/22-group-hidden-0.jpg": { + "bytes": 137291, + "sha256": "30112dfcead6736041c506fbe6f9f4abd67808bcd59ec96fb684839ed9fef071" + }, + "runs/run-1/raw/22-group-hidden.json": { + "bytes": 944, + "sha256": "0985c92ce81de4240451e074c361929a9ae90525d83feeba94c6a7022268683d" + }, + "runs/run-1/raw/23-hidden-saved-0.jpg": { + "bytes": 136419, + "sha256": "59e0770669d39cacc14ac5f958739d06cf0b647b02f096ff97ab7f65c3448758" + }, + "runs/run-1/raw/23-hidden-saved.json": { + "bytes": 938, + "sha256": "3f5e4122c005b0af25b54f91fa75640c1379afbcc2b5a26c36b89df91c354418" + }, + "runs/run-1/raw/24-hidden-placement-picker-0.jpg": { + "bytes": 137734, + "sha256": "f4fd098b9d64194846bfc3912a11ee20a09abd042bb8a33e8bc5a52e0b393a5f" + }, + "runs/run-1/raw/24-hidden-placement-picker.json": { + "bytes": 947, + "sha256": "f345669592b3efa382a9f688d0e97291c045c2734dba11ad0262ba1be3178273" + }, + "runs/run-1/raw/25-picker-dismissed-0.jpg": { + "bytes": 137694, + "sha256": "e5af5ad56705204d79352869f362ab430d1c5cbafce5ae1dba5093a8311728a8" + }, + "runs/run-1/raw/25-picker-dismissed.json": { + "bytes": 940, + "sha256": "317c8c7e5e1f9a0bb7ac1de51b8a75318ac29d67f81142d46214a013f53def18" + }, + "runs/run-1/raw/26-picker-click-dismissed-0.jpg": { + "bytes": 136300, + "sha256": "c5e1c4f8ed3fbb68b67429220d3a5db6a846f6753c297d194fcdeaba0ef268a0" + }, + "runs/run-1/raw/26-picker-click-dismissed.json": { + "bytes": 946, + "sha256": "938fe444bbff489676da4e36b8b9272f78739b520aca23264b7a2001c352f283" + }, + "runs/run-1/raw/27-hide-undo-0.jpg": { + "bytes": 145506, + "sha256": "712d6b3863bef24458e31be2b86fcc2b4e1f08a340ed78dda0c27f2d20992275" + }, + "runs/run-1/raw/27-hide-undo.json": { + "bytes": 943, + "sha256": "4b7c9088978c6bef6c60cfb9401f39b6c1460f92382a54db60c1e30bac97c4e6" + }, + "runs/run-1/raw/28-hide-undo-saved-0.jpg": { + "bytes": 145429, + "sha256": "848082bd3d40431dbd559896cf203c7d4a58385518b3ca02559a4b66633b8ccb" + }, + "runs/run-1/raw/28-hide-undo-saved.json": { + "bytes": 939, + "sha256": "8311eeb6bce344a989ffd7441ccfe75fd248d57a3e7013c8d7fd770850485d72" + }, + "runs/run-1/raw/29-hide-redo-0.jpg": { + "bytes": 136428, + "sha256": "280ccb922882c3419c84d7cae928d62c0cc1cd7a8b0b6a5a1245be0046895ae0" + }, + "runs/run-1/raw/29-hide-redo.json": { + "bytes": 943, + "sha256": "e238877bc772c25f2f05a2a4e75d2d78c349c2d1c27ab7fe49f05cc73599f435" + }, + "runs/run-1/raw/30-hide-redo-saved-0.jpg": { + "bytes": 136444, + "sha256": "5a81ae63d47528fdab27b987f1b9d996cc188f96b26c4d6b3009b4f637562ae4" + }, + "runs/run-1/raw/30-hide-redo-saved.json": { + "bytes": 941, + "sha256": "932f541a8e9364a50c714401a915717d69035cd9e15c415454d169749c0bc16f" + }, + "runs/run-1/raw/31-reopen-request-0.jpg": { + "bytes": 107440, + "sha256": "88b53c5f37b0107a1842c3b79b6258b255ef6bf41f6fd5df76fd8cc1d4cdd48f" + }, + "runs/run-1/raw/31-reopen-request.json": { + "bytes": 938, + "sha256": "c65e7668bad5c08953e6bf03ada8f304d9b5512751b8cb19657d84cf7d6f869c" + }, + "runs/run-1/raw/32-reopened-hidden-0.jpg": { + "bytes": 104737, + "sha256": "ef52c777acfc3acf9914e3703601d96e9fcc20095870852d02b696a50317c8ad" + }, + "runs/run-1/raw/32-reopened-hidden-1.jpg": { + "bytes": 4207, + "sha256": "147948d69961c42f1feea9449df477b8436250388a1ae070e744ed4709a49bf3" + }, + "runs/run-1/raw/32-reopened-hidden-2.jpg": { + "bytes": 4000, + "sha256": "9110bb88fff054f17fa7f2574a409f27755a72ffe6c14b42097db201a80d2b3f" + }, + "runs/run-1/raw/32-reopened-hidden.json": { + "bytes": 983, + "sha256": "e265715f1893419799423d265cb1d07d0554fa03fe25346cf5377669448aceea" + }, + "runs/run-1/raw/33-reopen-settled-0.jpg": { + "bytes": 104737, + "sha256": "ef52c777acfc3acf9914e3703601d96e9fcc20095870852d02b696a50317c8ad" + }, + "runs/run-1/raw/33-reopen-settled-1.jpg": { + "bytes": 4207, + "sha256": "147948d69961c42f1feea9449df477b8436250388a1ae070e744ed4709a49bf3" + }, + "runs/run-1/raw/33-reopen-settled-2.jpg": { + "bytes": 4000, + "sha256": "9110bb88fff054f17fa7f2574a409f27755a72ffe6c14b42097db201a80d2b3f" + }, + "runs/run-1/raw/33-reopen-settled.json": { + "bytes": 660, + "sha256": "9c254237908dbfa081000f6c44adc013120975388dc8902414caeb1028d1e23b" + }, + "runs/run-1/raw/34-reopened-source-0.jpg": { + "bytes": 123459, + "sha256": "dc824c52b07a8505c82420c581e87cfc6e0759428bceaf8cf84d7fcc981cfefc" + }, + "runs/run-1/raw/34-reopened-source.json": { + "bytes": 939, + "sha256": "b0fb2a67daa2eb4beef93ba181d5c6a73a734695a218f57725962d428ffe6bc2" + }, + "runs/run-1/raw/35-reopened-saved-0.jpg": { + "bytes": 121731, + "sha256": "0019c2970ab87c54760fd1ba922aa4471a598888635e835a3a56cc2ce966ba08" + }, + "runs/run-1/raw/35-reopened-saved.json": { + "bytes": 938, + "sha256": "e8382c2a32bae5febc5f90c0b09fe9353990c7d8b131d038bb0fc556c84b7ca3" + }, + "runs/run-1/raw/app.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/app.stdout.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/finished.json": { + "bytes": 393, + "sha256": "c5d9ee9618bab59c55cafa20ba8568b53efe3246d8d7eb2432fd3e2a95a0da03" + }, + "runs/run-1/raw/generation.json": { + "bytes": 1263, + "sha256": "e112c1aea1fe70f03a239597a494170b9f292bf57f1decf870c768d76a9eeb30" + }, + "runs/run-1/raw/generation.stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/generation.stdout.log": { + "bytes": 106, + "sha256": "461a61cfb7c5b139a790db5fba4c9fb98dd4951308c63365ef339a388d7e7b2c" + }, + "runs/run-1/raw/history-bytes.json": { + "bytes": 786, + "sha256": "a8948f0172aa550a5fb63a66396f771e1ba36f437fe76f5775bb35829f70219c" + }, + "runs/run-1/raw/processes.json": { + "bytes": 201, + "sha256": "4626c05bb8eb58e8d3acd1dc4157299d2c6c5eb0e0200e259a117a2517414cc0" + }, + "runs/run-1/raw/snapshots/baseline/result.json": { + "bytes": 2767, + "sha256": "779cfef18da513f0aa699975be2487435182bf063ad6c1635d92f557e23eeee4" + }, + "runs/run-1/raw/snapshots/baseline/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/baseline/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/baseline/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/baseline/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/baseline/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/baseline/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/baseline/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/baseline/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/baseline/source/views/diagram_state_view.arcadia": { + "bytes": 3781, + "sha256": "feda42f7f4329b925b7f87289b384f075705616a09a310a99c96e4177f6fee85" + }, + "runs/run-1/raw/snapshots/baseline/started.json": { + "bytes": 2365, + "sha256": "2f5c987ed6c8114d99751d6dd2cfa38c1b26496aa91043c06da3c0ae0d35ae82" + }, + "runs/run-1/raw/snapshots/baseline/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/baseline/stdout.log": { + "bytes": 453, + "sha256": "7e3c8332c974e194a96381c3e3b7118fd9f7436405688c068b00186d46449a1b" + }, + "runs/run-1/raw/snapshots/before-hide/result.json": { + "bytes": 2770, + "sha256": "a43fc74c00e4a47550aee83d7d5d67ab899d5e148e7bbc91ad3a0083b8eec0d7" + }, + "runs/run-1/raw/snapshots/before-hide/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/before-hide/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/before-hide/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/before-hide/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/before-hide/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/before-hide/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/before-hide/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/before-hide/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/before-hide/source/views/diagram_state_view.arcadia": { + "bytes": 3781, + "sha256": "feda42f7f4329b925b7f87289b384f075705616a09a310a99c96e4177f6fee85" + }, + "runs/run-1/raw/snapshots/before-hide/started.json": { + "bytes": 2368, + "sha256": "64a60222db7778d75e913869de0d727623caf91aff1f356c5056b9fb98355bf0" + }, + "runs/run-1/raw/snapshots/before-hide/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/before-hide/stdout.log": { + "bytes": 456, + "sha256": "357e77e5bea40d18e1927e4d1195dbd9dffac5374f286125535495a635e00dea" + }, + "runs/run-1/raw/snapshots/hidden/result.json": { + "bytes": 2760, + "sha256": "f428ba297e8a029722ac742e311f4517409e8e38467574bc81c9525ff275dcf0" + }, + "runs/run-1/raw/snapshots/hidden/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/hidden/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/hidden/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/hidden/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/hidden/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/hidden/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/hidden/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/hidden/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/hidden/source/views/diagram_state_view.arcadia": { + "bytes": 3531, + "sha256": "e0e79284e24387d257f7d72937ff849da16730f9d06a2ce1c2f43c73b1c29548" + }, + "runs/run-1/raw/snapshots/hidden/started.json": { + "bytes": 2358, + "sha256": "a8031748a9c2374d36f90dcfdc639eec6e4407f7f89a43357252500ac6a5aafa" + }, + "runs/run-1/raw/snapshots/hidden/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/hidden/stdout.log": { + "bytes": 446, + "sha256": "e04ab66dd7715906946086d4507987858a4d960327ceb9f4987c6e3629dadc00" + }, + "runs/run-1/raw/snapshots/hide-redo/result.json": { + "bytes": 2763, + "sha256": "4b8a0060987ea8fff904b244d5e28792382a223bc7473cf13e0679eae6f7c528" + }, + "runs/run-1/raw/snapshots/hide-redo/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/hide-redo/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/hide-redo/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/hide-redo/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/hide-redo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/hide-redo/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/hide-redo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/hide-redo/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/hide-redo/source/views/diagram_state_view.arcadia": { + "bytes": 3531, + "sha256": "e0e79284e24387d257f7d72937ff849da16730f9d06a2ce1c2f43c73b1c29548" + }, + "runs/run-1/raw/snapshots/hide-redo/started.json": { + "bytes": 2361, + "sha256": "a25b92331de234fc267412e5450e0c566f74b811304a6e43a7fd410527181c0a" + }, + "runs/run-1/raw/snapshots/hide-redo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/hide-redo/stdout.log": { + "bytes": 449, + "sha256": "a2f840d03f9813fb0c2f52db73ece05a62e7176da9ec29687e9e8cfe98ea28d1" + }, + "runs/run-1/raw/snapshots/hide-undo/result.json": { + "bytes": 2768, + "sha256": "d7d71c33427319b1711b2136d1db4aef5718ce1a2790b04eed9c641e9011b1c3" + }, + "runs/run-1/raw/snapshots/hide-undo/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/hide-undo/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/hide-undo/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/hide-undo/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/hide-undo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/hide-undo/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/hide-undo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/hide-undo/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/hide-undo/source/views/diagram_state_view.arcadia": { + "bytes": 3781, + "sha256": "feda42f7f4329b925b7f87289b384f075705616a09a310a99c96e4177f6fee85" + }, + "runs/run-1/raw/snapshots/hide-undo/started.json": { + "bytes": 2366, + "sha256": "8be41b35f8fbd9eecda396d30779b9050f91bcf99e2d8b642456f1020e95de8c" + }, + "runs/run-1/raw/snapshots/hide-undo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/hide-undo/stdout.log": { + "bytes": 454, + "sha256": "6613b0a845fa99299f8654c0060d169cefaf16c50962a877e70ac6de61bd4e10" + }, + "runs/run-1/raw/snapshots/move-redo/result.json": { + "bytes": 2764, + "sha256": "c90801f95afa47a513ed387b97a22a813c87c97f1dcfbe98dd19d7b842ddb9b3" + }, + "runs/run-1/raw/snapshots/move-redo/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/move-redo/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/move-redo/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/move-redo/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/move-redo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/move-redo/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/move-redo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/move-redo/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/move-redo/source/views/diagram_state_view.arcadia": { + "bytes": 4133, + "sha256": "d68617ffcf8a5421f432a225f171df0debf84d4b7d925ed4bd3c6e772bf2f138" + }, + "runs/run-1/raw/snapshots/move-redo/started.json": { + "bytes": 2362, + "sha256": "563ac107a3d36ef76841a50b46d55706f3735b0d90fcba996b9d8c8330bdb239" + }, + "runs/run-1/raw/snapshots/move-redo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/move-redo/stdout.log": { + "bytes": 506, + "sha256": "12d6629adf26066d1bd57c79081e236fd0c2aa6b2f91fd8b31166ac3c498012b" + }, + "runs/run-1/raw/snapshots/move-undo/result.json": { + "bytes": 2768, + "sha256": "629bfb2ccda6bbf45b4601bb80fb5cd0d4de3ae7b393c75270af1f9254b2c6b0" + }, + "runs/run-1/raw/snapshots/move-undo/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/move-undo/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/move-undo/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/move-undo/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/move-undo/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/move-undo/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/move-undo/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/move-undo/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/move-undo/source/views/diagram_state_view.arcadia": { + "bytes": 3781, + "sha256": "feda42f7f4329b925b7f87289b384f075705616a09a310a99c96e4177f6fee85" + }, + "runs/run-1/raw/snapshots/move-undo/started.json": { + "bytes": 2366, + "sha256": "4c0951301a0925ecd6910b4267d718212dc985f5d517d702b84a3408549a641e" + }, + "runs/run-1/raw/snapshots/move-undo/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/move-undo/stdout.log": { + "bytes": 454, + "sha256": "3eaa06dfb71a848b41ff52ba07b70b0c67d4ea6585a90410e6a5944d46b766ff" + }, + "runs/run-1/raw/snapshots/move/result.json": { + "bytes": 2759, + "sha256": "b6475c4f754aed0a551c0e6a474e8d1d62d479b40d997c9ce23bd95b3a4ff018" + }, + "runs/run-1/raw/snapshots/move/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/move/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/move/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/move/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/move/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/move/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/move/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/move/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/move/source/views/diagram_state_view.arcadia": { + "bytes": 4133, + "sha256": "d68617ffcf8a5421f432a225f171df0debf84d4b7d925ed4bd3c6e772bf2f138" + }, + "runs/run-1/raw/snapshots/move/started.json": { + "bytes": 2357, + "sha256": "7f51cd0a4dc037b47ed110546898f0cf3d7495332416607db9c2a739928e3a69" + }, + "runs/run-1/raw/snapshots/move/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/move/stdout.log": { + "bytes": 501, + "sha256": "ec0470cbcaf044dcbd76018b77ce00d6479f9e07d0165a09264465bb50c6996e" + }, + "runs/run-1/raw/snapshots/reopened-hidden/result.json": { + "bytes": 2769, + "sha256": "51b2a225dc10fa0d2c5481568ef272fb87bf0bef87a583d363badcf185651590" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/reopened-hidden/source/views/diagram_state_view.arcadia": { + "bytes": 3531, + "sha256": "e0e79284e24387d257f7d72937ff849da16730f9d06a2ce1c2f43c73b1c29548" + }, + "runs/run-1/raw/snapshots/reopened-hidden/started.json": { + "bytes": 2367, + "sha256": "93733f75546fa2fafcef90bb9b968e0ba96b5c521ed1035ef260e0df10a70efa" + }, + "runs/run-1/raw/snapshots/reopened-hidden/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/reopened-hidden/stdout.log": { + "bytes": 455, + "sha256": "1e3fed0f40c77e81b7aa443787f12c132519efaff128cf6cb3e0222575d932fd" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/result.json": { + "bytes": 2778, + "sha256": "e174aa5452873191caf62f18e01d3db9e13cc3cd4720cee1ea193ee6c67f52e1" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/oa/activities/activity_shared_work.arcadia": { + "bytes": 148, + "sha256": "bea5df0da731e2277ccb26f7949b43f5393d309d92364cc49746052bf5ff9881" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/oa/behavior/state_machine_machine.arcadia": { + "bytes": 2820, + "sha256": "268098238fd3ae9da7b29e4583b11947501a64628622a0ee89b792cabf0a67ed" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/oa/participants.arcadia": { + "bytes": 133, + "sha256": "0f7db5cb2f8ba9642a93182e53f30bd6c593099d1f82ce1f2c503bb617cf917c" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/project.arcadia": { + "bytes": 618, + "sha256": "f6612ff6fe68e2eea2609a9e5168f6295a4e817792f41755ea7e16c8f7b970c2" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/sa/functions/function_sa_function.arcadia": { + "bytes": 158, + "sha256": "9d88c6450101fb66a6b52da4b0a154c272c168ae76a40ed15ebed4631740f607" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/traceability.arcadia": { + "bytes": 534, + "sha256": "8346e4a434095e423c1409f4291fff213d354ddb4a6ae09283dba2bc31391368" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/views/diagram_sa_view.arcadia": { + "bytes": 331, + "sha256": "1f0e7af9ebb2889bf43f7ea0e57f7cf47ef1f0ee661056dc71716a1b50bd5145" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/views/diagram_state_other.arcadia": { + "bytes": 633, + "sha256": "b8014eadf8166f4f9ff6e5f215e7660b2652c27f4f80cc02a2fabfcdee8a4aa4" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/source/views/diagram_state_view.arcadia": { + "bytes": 3781, + "sha256": "feda42f7f4329b925b7f87289b384f075705616a09a310a99c96e4177f6fee85" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/started.json": { + "bytes": 2376, + "sha256": "1c0c2799b5d579e5ae07da8dbb589985cca9cfb6f8b880ab5bf0c4b0da306814" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/stderr.log": { + "bytes": 0, + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "runs/run-1/raw/snapshots/row-mixed-unchanged/stdout.log": { + "bytes": 464, + "sha256": "26b9c11ecda895b50cdcdcac984d9792e6149df95db7a5def42207bb73c367ce" + }, + "runs/run-1/raw/started.json": { + "bytes": 4006, + "sha256": "ae508e19b35b2a399c19099882cf01b8c511fcf6dfa3cebd8578477136d3831d" + } + } +} diff --git a/harness/experiments/state-multiselect-20260923/native/prepare.py b/harness/experiments/state-multiselect-20260923/native/prepare.py new file mode 100644 index 00000000..977546e5 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/native/prepare.py @@ -0,0 +1,131 @@ +"""Package explicit completed native runs; helpers are pinned data, never executed.""" +from pathlib import Path +import argparse +import json +import re +import zipfile +from verify import pin, parsed, need, norm, relative, safe, verify_data, verify_package + +HERE = Path(__file__).resolve().parent + + +def collect(root, runs, extra_unchanged=()): + native = root / ".local/state-multiselect-native" + candidates = [path for path in native.iterdir() if path.is_file() and path.suffix in (".py", ".js")] + candidates.extend(sorted((root / ".local/state-group-gates").glob("*.py"))) + helpers = [(path.suffix, path.read_bytes()) for path in candidates] + payload, specs = {}, {} + def add(name, data): + need(safe(name) and (name not in payload or payload[name] == data), "member collision: " + name) + payload[name] = data + def tree(folder, prefix): + need(folder.is_dir(), "missing directory: " + str(folder)) + for path in sorted(folder.rglob("*")): + need(not path.is_symlink(), "symlink evidence input") + if path.is_file(): + add(prefix + path.relative_to(folder).as_posix(), path.read_bytes()) + for run in runs: + folder = native / run + start = parsed((folder / "started.json").read_bytes()) + end = parsed((folder / "finished.json").read_bytes()) + need(end["exitCode"] == 0 and all(end[k] is True for k in + ("guiUnchanged", "checkerUnchanged", "fixtureUnchanged")), "unfinished/changed native run: " + run) + generation = parsed((folder / "generation.json").read_bytes()) + history = parsed((folder / "history-bytes.json").read_bytes()) + tree(folder, "runs/" + run + "/raw/") + fixture_relative = relative(generation["command"][2], start["cwd"]) + need(fixture_relative.startswith(".local/state-multiselect-native/"), "fixture outside native root") + tree(root / fixture_relative, "runs/" + run + "/fixture/") + spec = {"originalRun": norm(start["cwd"]) + "/.local/state-multiselect-native/" + run, + "originalFixture": norm(generation["command"][2]), "helpers": {}, "builds": {}, + "extraUnchangedSnapshots": list(extra_unchanged)} + for key, expected in [("helper", start["helper"]), ("capture", start["capture"]), + ("snapshotHelper", start["snapshotHelper"]), ("history", history["helper"])]: + matches = [(suffix, data) for suffix, data in helpers if pin(data) == expected] + need(bool(matches), "exact historical helper unavailable: " + key) + suffix, data = matches[0] + member = "helpers/" + expected["sha256"] + suffix + add(member, data) + spec["helpers"][key] = member + for kind in ("gui", "checker"): + recorded = start[kind] + rel = relative(recorded["receipt"], start["cwd"]) + need(rel.startswith(".local/state-group-gates/") and rel.endswith("/result.json"), "unexpected build receipt location") + receipt = root / rel + data = receipt.read_bytes() + need(pin(data) == recorded["receiptPin"], "historical build receipt changed") + prefix = "builds/" + pin(data)["sha256"] + "/" + add(prefix + "result.json", data) + inputs_data = (receipt.parent / "inputs.json").read_bytes() + need(pin(inputs_data) == recorded["inputs"], "historical build input map changed") + add(prefix + "inputs.json", inputs_data) + spec["builds"][kind] = {"receipt": prefix + "result.json", "inputs": prefix + "inputs.json", "originalRelative": rel} + build_helper = parsed(data)["helper"] + matching_helpers = [(suffix, value) for suffix, value in helpers if pin(value) == build_helper] + need(bool(matching_helpers), "exact historical build helper unavailable") + suffix, helper_data = matching_helpers[0] + helper_member = "helpers/" + build_helper["sha256"] + suffix + add(helper_member, helper_data) + spec["builds"][kind]["helper"] = helper_member + if kind == "checker": + # Read only the pinned historical example, not today's checkout. + archive_path = receipt.parent / "inputs.zip" + need(pin(archive_path.read_bytes()) == parsed(data)["archive"], "checker input archive pin") + source_path = "crates/oa-core/examples/state_multiselect_native.rs" + with zipfile.ZipFile(archive_path) as archive: + source = archive.read(source_path) + need(pin(source) == parsed(inputs_data)[source_path], "historical checker source pin") + member = "checker-source/" + pin(source)["sha256"] + ".rs" + add(member, source) + spec["checkerSource"] = {"inputPath": source_path, "member": member} + specs[run] = spec + manifest = {"schema": 1, "kind": "multiselect-native", "runs": specs, + "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 stream: + stream.write((json.dumps(value, indent=2) + "\n").encode()) + + +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, default=HERE) + parser.add_argument("--check-only", action="store_true") + parser.add_argument("--extra-unchanged", nargs="*", default=[], help="explicit additional unchanged snapshot labels, same for all runs") + args = parser.parse_args() + need(len(args.runs) == len(set(args.runs)) and all(re.fullmatch(r"run-[0-9]+", name) for name in args.runs), "explicit unique run-N names required") + payload, manifest, summary = collect(args.root.resolve(), args.runs, args.extra_unchanged) + if args.check_only: + print("PASS raw consistency; no archive written:", summary) + return + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + for name in ("evidence.zip", "manifest.json", "files.json"): + need(not (out / name).exists(), "existing output refused: " + name) + for name in ("prepare.py", "verify.py", "README.md"): + data = (HERE / name).read_bytes() + if (out / name).exists(): + need((out / name).read_bytes() == data, "different existing package tool") + else: + with (out / name).open("xb") as stream: + stream.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 evidence") + print("PASS package readback:", out, summary) + + +if __name__ == "__main__": + main() diff --git a/harness/experiments/state-multiselect-20260923/native/verify.py b/harness/experiments/state-multiselect-20260923/native/verify.py new file mode 100644 index 00000000..d896a07a --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/native/verify.py @@ -0,0 +1,240 @@ +"""Verify archived native receipts and bytes; do not rerun the GUI or checker.""" +from pathlib import Path, PurePosixPath +from datetime import datetime +import argparse +import hashlib +import json +import math +import stat +import zipfile + +BASE = {"started.json", "finished.json", "generation.json", "generation.stdout.log", + "generation.stderr.log", "app.stdout.log", "app.stderr.log", "processes.json", "history-bytes.json"} +GROUPS = [["baseline", "move-undo", "before-hide", "hide-undo"], ["move", "move-redo"], + ["hidden", "hide-redo", "reopened-hidden"]] +MODES = {name: mode for group, mode in zip(GROUPS, ["unchanged", "moved", "hide"]) for name in group} +OUTER = {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"} + + +def need(ok, message): + if not ok: + raise ValueError(message) + + +def pin(data): + return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def safe(name): + return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name + and not PurePosixPath(name).is_absolute() and all(ord(c) >= 32 for c in name) + and all(part not in ("", ".", "..") for part in name.split("/"))) + + +def parsed(data): + def unique(items): + result = {} + for key, value in items: + need(key not in result, "duplicate JSON key: " + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=unique) + + +def norm(path): + return path.replace("\\", "/").rstrip("/") + + +def relative(path, root): + prefix = norm(root) + "/" + need(norm(path).startswith(prefix), "recorded path outside owner: " + path) + value = norm(path)[len(prefix):] + need(safe(value), "unsafe recorded relative path") + return value + + +def ordered(*values): + times = [datetime.fromisoformat(value) for value in values] + return all(t.utcoffset() is not None for t in times) and times == sorted(times) + + +def zip_inventory(archive, members): + names = archive.namelist() + need(len(names) == len(set(names)) and set(names) == set(members), "exact archive members") + for info in archive.infolist(): + need(safe(info.filename) and not info.is_dir() and not stat.S_ISLNK(info.external_attr >> 16), "unsafe archive member") + need(info.file_size == members[info.filename]["bytes"], "archive member size") + + +def verify_data(manifest, read): + need(manifest["schema"] == 1 and manifest["kind"] == "multiselect-native", "manifest kind") + members, runs = manifest["members"], manifest["runs"] + need(bool(runs) and all(safe(run) and "/" not in run for run in runs), "run names") + need(all(safe(name) for name in members), "safe member names") + for name, expected in members.items(): + need(pin(read(name)) == expected, "member pin: " + name) + used = set() + def raw(name): + need(name in members, "missing member: " + name) + used.add(name) + return read(name) + def obj(name): + return parsed(raw(name)) + def tree(prefix): + return {name[len(prefix):]: raw(name) for name in members if name.startswith(prefix)} + def pinned_tree(prefix, expected): + actual = tree(prefix) + need({name: pin(data) for name, data in actual.items()} == expected, "exact pinned tree: " + prefix) + return actual + for run, spec in runs.items(): + prefix = "runs/" + run + "/raw/" + start, end = obj(prefix + "started.json"), obj(prefix + "finished.json") + generation, process = obj(prefix + "generation.json"), obj(prefix + "processes.json") + history = obj(prefix + "history-bytes.json") + need(end["exitCode"] == 0 and all(end[key] is True for key in + ("guiUnchanged", "checkerUnchanged", "fixtureUnchanged")), run + ": clean unchanged closure") + need(ordered(generation["start"], generation["finish"], process["CreationDate"], start["start"], end["finish"], history["verified"]), run + ": process/generation times") + need(process["ProcessId"] == start["pid"] and type(start["pid"]) is int and start["pid"] > 0, run + ": process identity") + need(norm(process["ExecutablePath"]) == norm(start["gui"]["binary"]) + and start["command"] == [start["gui"]["binary"]], run + ": GUI command") + need(spec["originalRun"] == norm(start["cwd"]) + "/.local/state-multiselect-native/" + run, run + ": original run mapping") + need(generation["exitCode"] == 0 and generation["checker"] == start["checker"], run + ": generation checker") + need(generation["command"][:2] == [start["checker"]["binary"], "generate"] + and len(generation["command"]) == 3 and norm(generation["command"][2]) == spec["originalFixture"], run + ": generation command") + for stream in ("stdout", "stderr"): + need(end[stream] == pin(raw(prefix + "app." + stream + ".log")), run + ": app stream") + need(generation[stream] == pin(raw(prefix + "generation." + stream + ".log")), run + ": generation stream") + fixture = pinned_tree("runs/" + run + "/fixture/", start["fixture"]) + need(set(fixture) == {"baseline.json", "manifest.json"} | {"source/" + name for name in + obj(prefix + "snapshots/baseline/result.json")["source"]}, run + ": fixture membership") + fixture_manifest = parsed(fixture["manifest.json"]) + need(fixture_manifest["fixture"] == "state-multiselect-native-20260923" + and fixture_manifest["source_entry"] == "project.arcadia", run + ": fixture identity") + need(parsed(fixture["baseline.json"])["id"] == fixture_manifest["ids"]["project"], run + ": baseline identity") + for key, expected in [("helper", start["helper"]), ("capture", start["capture"]), + ("snapshotHelper", start["snapshotHelper"]), ("history", history["helper"])]: + need(pin(raw(spec["helpers"][key])) == expected, run + ": helper " + key) + builds = {} + for kind, target in [("gui", "arcadia-studio"), ("checker", "state_multiselect_native")]: + recorded = start[kind] + build = obj(spec["builds"][kind]["receipt"]) + need(pin(raw(spec["builds"][kind]["receipt"])) == recorded["receiptPin"], run + ": build receipt pin") + need(relative(recorded["receipt"], start["cwd"]) == spec["builds"][kind]["originalRelative"], run + ": build path mapping") + need(build["exitCode"] == 0 and build["inputsUnchanged"] is True + and ordered(build["start"], build["finish"], generation["start"]), run + ": accepted preceding build") + need(build["inputs"] == recorded["inputs"] == pin(raw(spec["builds"][kind]["inputs"])), run + ": build input map") + need(pin(raw(spec["builds"][kind]["helper"])) == build["helper"], run + ": build helper pin") + inputs = obj(spec["builds"][kind]["inputs"]) + need(len(inputs) == build["inputCount"] and sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": build input count") + artifacts = [item for item in build["artifacts"] if item["cargo"]["target"]["name"] == target] + need(len(artifacts) == 1 and artifacts[0]["readback"] == recorded["pin"] + and artifacts[0]["cargo"]["executable"] == recorded["binary"], run + ": executable relationship") + builds[kind] = inputs + need(start["gui"]["inputs"] == start["checker"]["inputs"], run + ": matching GUI/checker inputs") + source = spec["checkerSource"] + need(source["inputPath"] == "crates/oa-core/examples/state_multiselect_native.rs" + and pin(raw(source["member"])) == builds["checker"][source["inputPath"]], run + ": archived exact checker source") + + top = {name[len(prefix):] for name in members if name.startswith(prefix) and "/" not in name[len(prefix):]} + captures = sorted(name for name in top - BASE if name.endswith(".json")) + need(bool(captures), run + ": missing captures") + expected_top = set(BASE) | set(captures) + captured_window = None + for name in captures: + capture = obj(prefix + name) + need(ordered(start["start"], capture["time"], end["finish"]), run + ": capture lifetime") + window = capture["window"] + app = window["app"] + need(isinstance(app, str) and app.startswith("process:") + and norm(app[len("process:"):]) == norm(start["gui"]["binary"]), run + ": captured executable relationship") + need(type(window["id"]) is int and window["id"] > 0, run + ": captured window identity") + if captured_window is None: + captured_window = window["id"] + need(window["id"] == captured_window, run + ": inconsistent captured window identity") + need(isinstance(capture["images"], list), run + ": capture images") + for image in capture["images"]: + filename = image["file"] + need(safe(filename) and "/" not in filename and filename.startswith(name[:-5] + "-") + and filename.endswith((".jpg", ".png")), run + ": capture image mapping") + need(type(image["width"]) is int and image["width"] > 0 + and type(image["height"]) is int and image["height"] > 0, run + ": image dimensions") + need(filename not in expected_top, run + ": duplicate image reference") + expected_top.add(filename) + raw(prefix + filename) + need(top == expected_top, run + ": extra/missing top-level run files") + + snapshots = {name[len(prefix + "snapshots/"):].split("/")[0] for name in members if name.startswith(prefix + "snapshots/")} + extras = spec["extraUnchangedSnapshots"] + need(isinstance(extras, list) and len(extras) == len(set(extras)) + and all(safe(label) and "/" not in label and label not in MODES for label in extras), run + ": explicit extra unchanged labels") + modes = dict(MODES, **{label: "unchanged" for label in extras}) + need(snapshots == set(modes), run + ": exact nine history plus declared unchanged snapshot labels") + states = {} + for label, mode in modes.items(): + folder = prefix + "snapshots/" + label + "/" + began, result = obj(folder + "started.json"), obj(folder + "result.json") + need(all(result.get(key) == value for key, value in began.items()), label + ": start/result") + need(result["exitCode"] == 0 and all(result[key] is True for key in + ("binaryUnchanged", "workingSourceUnchanged", "baselineUnchanged")), label + ": unchanged successful checker") + need(ordered(start["start"], result["start"], result["finish"], end["finish"]), label + ": checker lifetime") + need(result["buildReceipt"] == start["checker"]["receiptPin"] and result["binary"] == start["checker"]["pin"] + and result["helper"] == start["snapshotHelper"], label + ": checker provenance") + need(result["baseline"] == pin(fixture["baseline.json"]), label + ": baseline pin") + command = result["command"] + need(len(command) == 5 and command[:3] == [start["checker"]["binary"], "check", mode] + and norm(command[3]) == spec["originalFixture"] + "/baseline.json" + and norm(command[4]) == spec["originalRun"] + "/snapshots/" + label + "/source/project.arcadia", label + ": exact checker command") + for stream in ("stdout", "stderr"): + need(result[stream] == pin(raw(folder + stream + ".log")), label + ": stream pin") + need(not raw(folder + "stderr.log"), label + ": checker stderr") + report = obj(folder + "stdout.log") + need(report["result"] == "PASS" and report["comparison"] == "complete Project equality" + and report["mode"] == mode and report["baseline"] == command[3] and report["saved"] == command[4], label + ": positive checker report") + delta = report["inferred_common_delta"] + if mode == "moved": + need(isinstance(delta, dict) and set(delta) == {"x", "y"} + and all(type(v) in (int, float) and math.isfinite(v) and abs(v) <= 300 for v in delta.values()) + and math.hypot(delta["x"], delta["y"]) >= 0.01, label + ": inferred State movement bounds") + else: + need(delta is None, label + ": unexpected inferred delta") + states[label] = pinned_tree(folder + "source/", result["source"]) + need(len(states[label]) == history["sourceFilesPerSnapshot"] == len(fixture) - 2, label + ": source count") + expected = {folder + name for name in ("started.json", "result.json", "stdout.log", "stderr.log")} + expected.update(folder + "source/" + name for name in states[label]) + need({name for name in members if name.startswith(folder)} == expected, label + ": extra snapshot files") + need(all(states[label] == states["baseline"] for label in extras), run + ": extra snapshots equal baseline bytes") + need(history["status"] == "PASS" and history["equalByteGroups"] == GROUPS, run + ": history groups") + for group in GROUPS: + need(all(states[label] == states[group[0]] for label in group), run + ": actual history bytes " + str(group)) + need(all(states[a[0]] != states[b[0]] for i, a in enumerate(GROUPS) for b in GROUPS[i + 1:]), run + ": distinct history states") + need(states["baseline"] == {name[7:]: data for name, data in fixture.items() if name.startswith("source/")}, run + ": baseline source exact") + changed = {label: sorted(name for name in set(states["baseline"]) | set(states[label]) + if states["baseline"].get(name) != states[label].get(name)) for label in ("move", "hidden")} + need(changed == history["changedFilesFromBaseline"] + == {label: ["views/diagram_state_view.arcadia"] for label in ("move", "hidden")}, run + ": changed source files") + print(run, "closed exit 0;", len(captures), "captures;", len(modes), "positive snapshots; exact three-group source history") + need(used == set(members), "unreferenced/extra package members") + return {"runs": len(runs), "members": len(members)} + + +def verify_package(root): + files = parsed((root / "files.json").read_bytes()) + need(set(files) == OUTER, "outer files") + for name, expected in files.items(): + need(pin((root / name).read_bytes()) == expected, "outer pin: " + name) + manifest = parsed((root / "manifest.json").read_bytes()) + with zipfile.ZipFile(root / "evidence.zip") as archive: + zip_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 native evidence byte/receipt consistency:", verify_package(args.package)) + print("No GUI/checker execution; actions and native acceptance require the separate parent review.") + + +if __name__ == "__main__": + main() diff --git a/harness/experiments/state-multiselect-20260923/source/.gitattributes b/harness/experiments/state-multiselect-20260923/source/.gitattributes new file mode 100644 index 00000000..62544950 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/.gitattributes @@ -0,0 +1 @@ +* -text diff --git a/harness/experiments/state-multiselect-20260923/source/README.md b/harness/experiments/state-multiselect-20260923/source/README.md new file mode 100644 index 00000000..16025bb6 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/README.md @@ -0,0 +1,58 @@ +# State multiselect source identity evidence + +The source receipts bind two historical captured input maps to exact Git trees: + +| Gate | Commit | Inputs | Input-map SHA-256 | +| --- | --- | ---: | --- | +| desktop-2 | 2a3b9ba822a49ead233c4b5f98412a07c1c1da12 | 733 | 9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a | +| lint-2 | 3480b54201a5f361cca0bcd0d45dd6546a7a7bc9 | 735 | 4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf | + +The original receipts are preserved without rewriting. The verifier fixes their +exact byte pins, commit IDs, map pins/counts and explicitCompileTimeFixtures. +The historical 733-input scope excludes two compile-time fixtures; the final +735-input scope explicitly includes both: + +- harness/experiments/edge-attachments-20260922/native-fixture.arcadia +- harness/experiments/textual-authoring-20260911/alarm-response.arcadia + +Matching an old Git tree does not correct those historical scope omissions or +establish the final source's behavior. A gate applies to its captured input map. +Later native build maps must independently match the final 735-input map before +using this source identity evidence for those builds. + +From this tool directory, prepare against completed raw captures without writing: + +``` +python -B prepare.py --raw-gates CHECKOUT/.local/state-group-gates --repo CHECKOUT --check-only +``` + +After the strict sibling checks package is complete, add --checks CHECKS_PACKAGE, +omit --check-only, and provide --output NEW_SOURCE_PACKAGE. Existing output is +refused. Preparation preserves the source receipts, tools and README, writes +files.json pins, and performs readback. No ZIP is created by the source preparer. + +A completed portable package can be verified without the original checkout: + +``` +python -B SOURCE_PACKAGE/verify.py +``` + +By default it reads the sibling ../checks package; override with --checks PATH. +It verifies checks outer/member pins, capsule relationships and both selected +nested source ZIPs against their exact maps. It checks source receipt identity, +accepted unchanged gate flags, scope/counts and all source package pins. Run the +sibling checks verifier separately for full process/helper/stream relationships. + +Add --repo CHECKOUT to independently repeat the complete filtered Git inventory +and exact blob-byte comparison against both pinned commits already present there. +Without --repo, verification checks historical receipt/capsule consistency and +explicitly reports that Git comparison was not rerun. No archived helpers or +source executables are run; no build, checkout, extraction or network is needed. + +verify-committed-inputs.py remains the generic comparator used to create source +receipts. Its --include-fixtures option includes both explicit fixtures; omission +keeps the historical scope. It accepts explicit gate/full commit IDs, defaults to +read-only comparison, and writes only an optional new --output receipt. + +Receipts and pins establish internal byte consistency, not external authentication, +hermetic toolchains, test execution, native usability or complete issue acceptance. diff --git a/harness/experiments/state-multiselect-20260923/source/files.json b/harness/experiments/state-multiselect-20260923/source/files.json new file mode 100644 index 00000000..e5e36f43 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/files.json @@ -0,0 +1,30 @@ +{ + ".gitattributes": { + "bytes": 9, + "sha256": "8b9c3028d6467ff3be7c793141927841d70f47f52d0590677438f488ca154992" + }, + "README.md": { + "bytes": 3092, + "sha256": "b122e41bb9c52b4384e09cd62ee4115a68ff16d5aefee2f92fe632b07276d017" + }, + "prepare.py": { + "bytes": 2100, + "sha256": "1be2f2a402b493a1c9700044f1119ed8ccddaa02baec2ecd25a185aba6253112" + }, + "source-desktop-2.json": { + "bytes": 428, + "sha256": "6012bdc52a3843c9cfd5f3c80ec4270a7c0f1e13ef4738b0e3f4ed048ff8d885" + }, + "source-lint-2.json": { + "bytes": 583, + "sha256": "e0a6deab68683278965643f6f34a1038fcea25c1f242b6f75f5559b7697ec7d6" + }, + "verify-committed-inputs.py": { + "bytes": 5712, + "sha256": "3cd4f0155cd29c6786f467562d458a380084b2a510eac6454a29388f8f048772" + }, + "verify.py": { + "bytes": 10126, + "sha256": "e670dc47e89a6ad5cdf7f9adfb6a50797270e475d6b312f159f7aef941ae723b" + } +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/source/prepare.py b/harness/experiments/state-multiselect-20260923/source/prepare.py new file mode 100644 index 00000000..7c9a2913 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/prepare.py @@ -0,0 +1,33 @@ +"""Prepare pinned source receipts after raw/sibling capture validation; never build.""" +from pathlib import Path +import argparse, json +from verify import HERE, SPECS, TOOLS, FILES, need, pin, verify_data, raw_captures, packaged, verify_package + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--raw-gates',type=Path,required=True,help='original raw gates and source-RUN.json receipts') + parser.add_argument('--checks',type=Path,help='completed sibling checks package; required when writing') + parser.add_argument('--repo',type=Path,help='optionally rerun exact committed Git-blob comparison') + parser.add_argument('--output',type=Path,help='new source package directory') + parser.add_argument('--check-only',action='store_true') + args=parser.parse_args() + receipts={run:(args.raw_gates/('source-'+run+'.json')).read_bytes() for run in SPECS} + captured=packaged(args.checks) if args.checks else raw_captures(args.raw_gates) + summary=verify_data(receipts,captured,args.repo) + if args.check_only: + print('PASS source check-only; no package written:',summary); return + need(args.checks is not None,'written portable source package requires completed --checks package') + need(args.output is not None,'explicit new --output required') + out=args.output.resolve(); need(not out.exists(),'existing package refused') + payload={name:(HERE/name).read_bytes() for name in TOOLS} + payload.update({'source-'+run+'.json':data for run,data in receipts.items()}) + need(set(payload)==FILES,'exact source files') + out.mkdir(parents=True) + for name,data in payload.items(): + with (out/name).open('xb') as stream: stream.write(data) + with (out/'files.json').open('x',encoding='utf-8') as stream: + json.dump({name:pin(data) for name,data in sorted(payload.items())},stream,indent=2) + # Git comparison was already performed above when requested; readback is byte-only. + verify_package(out,args.checks) + print('PASS written source package readback:',out,summary) +if __name__=='__main__': main() diff --git a/harness/experiments/state-multiselect-20260923/source/source-desktop-2.json b/harness/experiments/state-multiselect-20260923/source/source-desktop-2.json new file mode 100644 index 00000000..e583b3eb --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/source-desktop-2.json @@ -0,0 +1,12 @@ +{ + "checked": "2026-09-23T10:37:38.419535+00:00", + "commit": "2a3b9ba822a49ead233c4b5f98412a07c1c1da12", + "gate": "desktop-2", + "inputCount": 733, + "inputMap": { + "bytes": 122682, + "sha256": "9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a" + }, + "comparison": "Exact committed Git blobs equal every captured input; same complete filtered inventory", + "explicitCompileTimeFixtures": [] +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/source/source-lint-2.json b/harness/experiments/state-multiselect-20260923/source/source-lint-2.json new file mode 100644 index 00000000..5576097b --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/source-lint-2.json @@ -0,0 +1,15 @@ +{ + "checked": "2026-09-23T10:37:40.478617+00:00", + "commit": "3480b54201a5f361cca0bcd0d45dd6546a7a7bc9", + "gate": "lint-2", + "inputCount": 735, + "inputMap": { + "bytes": 123053, + "sha256": "4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf" + }, + "comparison": "Exact committed Git blobs equal every captured input; same complete filtered inventory", + "explicitCompileTimeFixtures": [ + "harness/experiments/edge-attachments-20260922/native-fixture.arcadia", + "harness/experiments/textual-authoring-20260911/alarm-response.arcadia" + ] +} \ No newline at end of file diff --git a/harness/experiments/state-multiselect-20260923/source/verify-committed-inputs.py b/harness/experiments/state-multiselect-20260923/source/verify-committed-inputs.py new file mode 100644 index 00000000..26ae8f64 --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/verify-committed-inputs.py @@ -0,0 +1,121 @@ +"""Compare captured input maps to exact named Git trees; no builds or checkouts.""" +from pathlib import Path, PurePosixPath +import argparse +import hashlib +import json +import os +import subprocess +import zipfile + +HERE = Path(__file__).resolve().parent +COMPARISON = "Exact committed Git blobs equal every captured input; same complete filtered inventory" +EXPLICIT_FIXTURES = { + "harness/experiments/edge-attachments-20260922/native-fixture.arcadia", + "harness/experiments/textual-authoring-20260911/alarm-response.arcadia", +} +INCLUDE_FIXTURES = False + + +def need(ok, message): + if not ok: + raise ValueError(message) + + +def pin(data): + return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def parsed(data): + def unique(items): + result = {} + for key, value in items: + need(key not in result, "duplicate JSON key: " + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=unique) + + +def safe(name): + return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name + and not PurePosixPath(name).is_absolute() + and all(part not in ("", ".", "..") for part in name.split("/"))) + + +def relevant(path): + return (INCLUDE_FIXTURES and path in EXPLICIT_FIXTURES) or path in ("Cargo.toml", "Cargo.lock", "rust-toolchain.toml") or path.startswith( + ("crates/", "build/", ".cargo/", "scripts/", "docs/", "examples/", "harness/fixtures/", "harness/prototypes/")) + + +def git(repo, *arguments, data=None): + environment = dict(os.environ, GIT_NO_LAZY_FETCH="1") + return subprocess.run(["git", *arguments], cwd=repo, input=data, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True, env=environment).stdout + + +def compare_commit(repo, commit, inputs): + need(git(repo, "rev-parse", "--verify", commit + "^{commit}").decode().strip() == commit, "named commit unavailable") + entries = git(repo, "ls-tree", "-r", "--full-tree", "-z", commit).split(b"\0") + blobs = {} + for entry in filter(None, entries): + header, path_bytes = entry.split(b"\t", 1) + path = path_bytes.decode("utf-8") + if relevant(path): + _mode, kind, oid = header.split() + need(kind == b"blob", "relevant non-blob tree entry: " + path) + need(path not in blobs, "duplicate tree path") + blobs[path] = oid + need(set(blobs) == set(inputs), "complete filtered Git inventory differs; missing=" + + repr(sorted(set(inputs) - set(blobs))) + "; additional=" + repr(sorted(set(blobs) - set(inputs)))) + # Object IDs avoid path quoting and commit:path interpretation in batch input. + paths = sorted(blobs) + data = git(repo, "cat-file", "--batch", data=b"".join(blobs[path] + b"\n" for path in paths)) + offset, actual = 0, {} + for path in paths: + end = data.index(b"\n", offset) + oid, kind, size_bytes = data[offset:end].split() + size = int(size_bytes) + need(oid == blobs[path] and kind == b"blob" and size >= 0, "Git batch object relationship") + content = data[end + 1:end + 1 + size] + need(len(content) == size and data[end + 1 + size:end + 2 + size] == b"\n", "Git batch framing") + actual[path] = pin(content) + offset = end + 2 + size + need(offset == len(data), "trailing Git batch bytes") + different = [path for path in paths if actual[path] != inputs[path]] + need(not different, "captured bytes differ from named commit: " + repr(different)) + + + +def main(): + import datetime + global INCLUDE_FIXTURES + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", type=Path, required=True) + parser.add_argument("--raw-gates", type=Path, required=True) + parser.add_argument("--gate", required=True) + parser.add_argument("--commit", required=True, help="exact full commit object ID, not a branch") + parser.add_argument("--include-fixtures", action="store_true", help="include both compile-time fixtures for the revised 735-input scope; default preserves historical 733-input scope") + parser.add_argument("--output", type=Path, help="optional new receipt file; otherwise read-only check") + args = parser.parse_args() + INCLUDE_FIXTURES = args.include_fixtures + need(safe(args.gate) and "/" not in args.gate, "safe gate name") + folder = args.raw_gates / args.gate + map_bytes = (folder / "inputs.json").read_bytes() + result = parsed((folder / "result.json").read_bytes()) + inputs = parsed(map_bytes) + need(result["exitCode"] == 0 and result["inputsUnchanged"] is True, "accepted unchanged gate required") + need(result["inputs"] == pin(map_bytes) and result["inputCount"] == len(inputs), "map relationship") + need(all(safe(path) and relevant(path) for path in inputs), "input scope") + need(sum(path.startswith("crates/vendor/iced_widget/") for path in inputs) == 71, "vendor inventory") + compare_commit(args.repo, args.commit, inputs) + report = {"checked": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "commit": args.commit, "gate": args.gate, "inputCount": len(inputs), + "inputMap": pin(map_bytes), "comparison": COMPARISON, + "explicitCompileTimeFixtures": sorted(EXPLICIT_FIXTURES) if INCLUDE_FIXTURES else []} + if args.output: + with args.output.open("x", encoding="utf-8") as stream: + json.dump(report, stream, indent=2) + print("PASS exact Git tree/blobs:", json.dumps(report)) + print("Source identity only; separately verify capture/archive consistency. No build, checkout or native acceptance claim.") + +if __name__ == "__main__": + main() diff --git a/harness/experiments/state-multiselect-20260923/source/verify.py b/harness/experiments/state-multiselect-20260923/source/verify.py new file mode 100644 index 00000000..74648a5c --- /dev/null +++ b/harness/experiments/state-multiselect-20260923/source/verify.py @@ -0,0 +1,173 @@ +"""Verify pinned State source receipts against sibling capture capsules; optionally compare Git blobs.""" +from pathlib import Path, PurePosixPath +import argparse +import hashlib +import json +import os +import subprocess +import zipfile +import io +import stat + +HERE = Path(__file__).resolve().parent +COMPARISON = "Exact committed Git blobs equal every captured input; same complete filtered inventory" +EXPLICIT_FIXTURES = { + "harness/experiments/edge-attachments-20260922/native-fixture.arcadia", + "harness/experiments/textual-authoring-20260911/alarm-response.arcadia", +} +INCLUDE_FIXTURES = False + + +def need(ok, message): + if not ok: + raise ValueError(message) + + +def pin(data): + return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()} + + +def parsed(data): + def unique(items): + result = {} + for key, value in items: + need(key not in result, "duplicate JSON key: " + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=unique) + + +def safe(name): + return (isinstance(name, str) and bool(name) and "\\" not in name and ":" not in name + and not PurePosixPath(name).is_absolute() + and all(part not in ("", ".", "..") for part in name.split("/"))) + + +def relevant(path): + return (INCLUDE_FIXTURES and path in EXPLICIT_FIXTURES) or path in ("Cargo.toml", "Cargo.lock", "rust-toolchain.toml") or path.startswith( + ("crates/", "build/", ".cargo/", "scripts/", "docs/", "examples/", "harness/fixtures/", "harness/prototypes/")) + + +def git(repo, *arguments, data=None): + environment = dict(os.environ, GIT_NO_LAZY_FETCH="1") + return subprocess.run(["git", *arguments], cwd=repo, input=data, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True, env=environment).stdout + + +def compare_commit(repo, commit, inputs): + need(git(repo, "rev-parse", "--verify", commit + "^{commit}").decode().strip() == commit, "named commit unavailable") + entries = git(repo, "ls-tree", "-r", "--full-tree", "-z", commit).split(b"\0") + blobs = {} + for entry in filter(None, entries): + header, path_bytes = entry.split(b"\t", 1) + path = path_bytes.decode("utf-8") + if relevant(path): + _mode, kind, oid = header.split() + need(kind == b"blob", "relevant non-blob tree entry: " + path) + need(path not in blobs, "duplicate tree path") + blobs[path] = oid + need(set(blobs) == set(inputs), "complete filtered Git inventory differs; missing=" + + repr(sorted(set(inputs) - set(blobs))) + "; additional=" + repr(sorted(set(blobs) - set(inputs)))) + # Object IDs avoid path quoting and commit:path interpretation in batch input. + paths = sorted(blobs) + data = git(repo, "cat-file", "--batch", data=b"".join(blobs[path] + b"\n" for path in paths)) + offset, actual = 0, {} + for path in paths: + end = data.index(b"\n", offset) + oid, kind, size_bytes = data[offset:end].split() + size = int(size_bytes) + need(oid == blobs[path] and kind == b"blob" and size >= 0, "Git batch object relationship") + content = data[end + 1:end + 1 + size] + need(len(content) == size and data[end + 1 + size:end + 2 + size] == b"\n", "Git batch framing") + actual[path] = pin(content) + offset = end + 2 + size + need(offset == len(data), "trailing Git batch bytes") + different = [path for path in paths if actual[path] != inputs[path]] + need(not different, "captured bytes differ from named commit: " + repr(different)) + + + +SPECS = {'desktop-2': {'commit': '2a3b9ba822a49ead233c4b5f98412a07c1c1da12', 'inputCount': 733, 'inputMap': {'bytes': 122682, 'sha256': '9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a'}, 'explicitCompileTimeFixtures': [], 'receiptPin': {'bytes': 428, 'sha256': '6012bdc52a3843c9cfd5f3c80ec4270a7c0f1e13ef4738b0e3f4ed048ff8d885'}}, 'lint-2': {'commit': '3480b54201a5f361cca0bcd0d45dd6546a7a7bc9', 'inputCount': 735, 'inputMap': {'bytes': 123053, 'sha256': '4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf'}, 'explicitCompileTimeFixtures': ['harness/experiments/edge-attachments-20260922/native-fixture.arcadia', 'harness/experiments/textual-authoring-20260911/alarm-response.arcadia'], 'receiptPin': {'bytes': 583, 'sha256': 'e0a6deab68683278965643f6f34a1038fcea25c1f242b6f75f5559b7697ec7d6'}}} +TOOLS = {".gitattributes", "prepare.py", "verify.py", "verify-committed-inputs.py", "README.md"} +FILES = TOOLS | {"source-" + run + ".json" for run in SPECS} + +def inventory(archive, expected): + names = archive.namelist() + need(len(names) == len(set(names)) and set(names) == set(expected), "exact ZIP inventory") + for item in archive.infolist(): + need(safe(item.filename) and not item.is_dir() and not stat.S_ISLNK(item.external_attr >> 16), "safe ZIP member") + need(pin(archive.read(item.filename)) == expected[item.filename], "ZIP member pin: " + item.filename) + +def verify_data(receipts, captured, repo=None): + global INCLUDE_FIXTURES + need(set(receipts) == set(captured) == set(SPECS), "exact source run inventory") + for run, spec in SPECS.items(): + receipt_bytes = receipts[run] + need(pin(receipt_bytes) == spec["receiptPin"], run + ": exact historical receipt bytes") + receipt = parsed(receipt_bytes) + need(receipt["gate"] == run and receipt["comparison"] == COMPARISON, run + ": receipt identity") + need(all(receipt[key] == spec[key] for key in ("commit", "inputCount", "inputMap", "explicitCompileTimeFixtures")), run + ": pinned receipt relationships") + map_bytes, result, archive_bytes = captured[run] + need(pin(map_bytes) == spec["inputMap"] == result["inputs"], run + ": exact captured input map") + need(result["exitCode"] == 0 and result["inputsUnchanged"] is True, run + ": accepted unchanged gate") + inputs = parsed(map_bytes) + need(len(inputs) == spec["inputCount"] == result["inputCount"], run + ": input counts") + INCLUDE_FIXTURES = bool(spec["explicitCompileTimeFixtures"]) + need(spec["explicitCompileTimeFixtures"] == (sorted(EXPLICIT_FIXTURES) if INCLUDE_FIXTURES else []), run + ": exact fixture scope") + need(all(safe(name) and relevant(name) for name in inputs), run + ": input scope") + need((EXPLICIT_FIXTURES & set(inputs)) == (EXPLICIT_FIXTURES if INCLUDE_FIXTURES else set()), run + ": explicit fixture membership") + need(sum(name.startswith("crates/vendor/iced_widget/") for name in inputs) == 71, run + ": vendor count") + need(pin(archive_bytes) == result["archive"], run + ": source capsule pin") + with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive: + inventory(archive, inputs) + if repo is not None: + compare_commit(repo, spec["commit"], inputs) + print("PASS", run, spec["commit"], len(inputs), "source pins" + (" and exact Git blobs" if repo is not None else " (Git comparison not rerun)")) + return {"runs": len(SPECS), "gitCompared": repo is not None} + +def raw_captures(gates): + return {run: ((gates/run/"inputs.json").read_bytes(), parsed((gates/run/"result.json").read_bytes()), + (gates/run/"inputs.zip").read_bytes()) for run in SPECS} + +def packaged(checks): + files = parsed((checks/"files.json").read_bytes()) + need(set(files) == {"prepare.py", "verify.py", "README.md", "manifest.json", "evidence.zip"}, "checks outer inventory") + for name, expected in files.items(): + need(pin((checks/name).read_bytes()) == expected, "checks outer pin: " + name) + manifest = parsed((checks/"manifest.json").read_bytes()) + need(manifest["schema"] == 1 and manifest["kind"] == "multiselect-cargo-gates", "checks kind") + logical, members = manifest["logicalFiles"], manifest["members"] + need(all(safe(name) and safe(target) for name,target in logical.items()) and set(logical.values()) == set(members), "checks logical inventory") + captured = {} + with zipfile.ZipFile(checks/"evidence.zip") as archive: + inventory(archive, members) + for run in SPECS: + prefix = "cargo/" + run + "/" + result = parsed(archive.read(logical[prefix + "result.json"])) + spec = manifest["runs"][run] + need(all(result[key] == spec[key] for key in ("command", "head", "exitCode", "inputCount")), run + ": checks relationships") + for name in ("inputs.json", "inputs.zip"): + member = logical[prefix+name] + need(member == "capsules/" + members[member]["sha256"] + Path(name).suffix, run + ": source capsule mapping") + captured[run] = (archive.read(logical[prefix+"inputs.json"]), result, archive.read(logical[prefix+"inputs.zip"])) + return captured + +def verify_package(package, checks, repo=None): + files = parsed((package/"files.json").read_bytes()) + need(set(files) == FILES, "source outer inventory") + need({p.name for p in package.iterdir()} == FILES | {"files.json"}, "source exact package inventory") + for name, expected in files.items(): + need((package/name).is_file() and not (package/name).is_symlink() and pin((package/name).read_bytes()) == expected, "source outer pin: " + name) + return verify_data({run:(package/("source-"+run+".json")).read_bytes() for run in SPECS}, packaged(checks), repo) + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("package", nargs="?", type=Path, default=HERE) + parser.add_argument("--checks", type=Path, help="sibling checks package; default PACKAGE/../checks") + parser.add_argument("--repo", type=Path, help="optional repository containing both exact commits, for renewed Git-blob comparison") + args = parser.parse_args() + print("PASS source evidence:", verify_package(args.package, args.checks or args.package.parent/"checks", args.repo)) + print("No builds, checkouts, helper execution or native acceptance claim. Run the checks verifier separately for full gate relationships.") + +if __name__ == "__main__": + main() diff --git a/harness/multiselect-architecture-review-20260923.md b/harness/multiselect-architecture-review-20260923.md new file mode 100644 index 00000000..f7df87ac --- /dev/null +++ b/harness/multiselect-architecture-review-20260923.md @@ -0,0 +1,229 @@ +# Architecture multi-selection critique — 2026-09-23 + +Independent read-only review of `b8685416f56e53e30bd11ecf93a54cea10ac8458` +for issue #29 by `transition_picker_review`. This is a proposal, not passing +implementation or native evidence. Only this note was written; no Cargo, GUI, +production changes, or coordination posts were made. The parent owns synthesis. +Architecture geometry can be a foundation, but does not complete the requested +coherent outcome across Allocation and Behavior. See the parallel +[Behavior review](multiselect-behavior-design-20260923.md). + +## Verified constraints + +| Boundary | Evidence at reviewed commit | Consequence | +| --- | --- | --- | +| Selection | `app.rs:790–835,1107,2477` uses semantic `selected` and one `selected_node`; `refresh_draft` derives placement again from semantic identity. | A new group cannot be inferred from the inspector selection or rebuilt as a singleton after each refresh. | +| Identity | `validation.rs:794` permits one placement per element per diagram; `projection.rs:169` resolves port endpoints to their owner placements. | Placement identity and active diagram still matter. A port does not become a separately movable card. | +| Ordinary movement | `diagram.rs:463,985,1166` previews one root plus descendants; `app.rs:3296` submits `MoveNode` against the then-active diagram after `apply()`. | New group intents need captured context and exact preview/commit parity, not repeated old messages. | +| Routes | `projection.rs:191` translates pins only when both actual endpoint owners share the same nonzero delta; State adds boundary checks. | A single simultaneous displacement map is necessary for unrelated selected endpoints. Labels/attachments retain relative data. | +| Allocation | `diagram/work_drag.rs:65` captures a work scope; `app/allocation_drag.rs:429,568` selects one work root on press and after commit. | Existing handlers would collapse a group. Work-body drag changes leaf allocations, not merely coordinates. | +| Admission | `app/allocation_drag.rs:353–411` checks owner, drafts, snapshot, context, camera and lifetime; `diagram/work_drag.rs:118` cancels on view/input changes. | Reuse this admission discipline, including rejection before shared state mutation. | +| Focus | `diagram/focus.rs:195` drops all keyboard events when canvas lacks focus. | Canvas-local modifier tracking alone misses Shift pressed while the inspector owns focus. | +| Removal | `app/diagram_removal.rs:114–260` distinguishes placement Hide from confirmed semantic Delete and captures one target/token. | Group removal needs an explicit set and a single combined confirmation. | + +## Selection contract + +Use transient selection scoped to document-instance and surface identity, with +ordered unique typed targets and one primary member for the existing inspector. +For ordinary diagrams, movable members are exact node placements. The owner +must distinguish a reopened document even if its project UUID is unchanged. +Selection ownership should not itself be tied to model revision: surviving +members should remain selected after a successful group move or primary-property +edit. Gesture authority must additionally capture revision/snapshot, epoch, +selection generation, camera and actual canvas bounds. + +Recommended interactions, consistent across applicable families: + +- Plain click an unselected card replaces selection. Shift+click toggles it; + adding makes it primary, and removing the primary promotes the most recent + remaining member. Shift selection must precede port/route/reorder handle logic. +- Pressing an already selected card preserves the set so dragging works. The + synthesis chooses collapse to that member on a completed stationary release; + this is acceptable consistently across families. A cancelled, refused, or + threshold-crossing drag must retain the group even if it ends at its origin. +- Right-click a member retains the group. Right-click elsewhere selects that + target before opening its menu. A stationary blank click clears; a pan gesture + retains selection. Do not replace existing blank-drag pan with marquee silently. +- Paint every member, distinguish primary without relying only on color, and show + the count. Inspector edits remain explicitly primary-only. Do not expose + singleton commands under a group label or imply bulk property editing. +- Track modifiers passively through the relevant input owner even before it has + keyboard focus; this does not grant permission for canvas shortcuts in text + fields. Freeze press-time modifiers and clear them on window focus loss. + +Typed edge/port selection can be shared with other families, but command +capabilities must remain explicit. Edges do not have node coordinates; port +anchors are owner-local. A mixed set must never silently move only its primary +or eligible subset. If placement-only grouping is delivered first, report that +scope and give an explicit unavailable-action reason for other target types. + +## Simultaneous movement and alternatives + +Prefer a pure planner which accepts the original diagram, selected placement +IDs and one delta, unions roots with visual descendants, translates each placement +once, calls `translated_routes` once on the original geometry, and emits one +`Batch(EditNode..., SetEdgeRoute...)`. Use the same plan for preview and commit. +Snap one shared delta, based on the grabbed anchor, rather than snapping each +node independently and changing their spacing. Nonselected records and other +diagrams must remain exact. + +Sequential `Batch(MoveNode(A), MoveNode(B))` is unsuitable: parent+child can move +twice, and a route between unrelated selected endpoints never sees both owners +in the same displacement map. A dedicated serialized `MoveNodes` command could +eventually express public API intent, but adds schema/documentation/compatibility +cost without being needed for the GUI's atomic transaction. Selection itself +does not belong in the model/source schema. An intent command also would not +remove the need for preview planning or stale-context checks. + +Use visual ancestry for placement closure, semantic work ancestry for allocation +leaf normalization, and semantic deletion dependencies for model removal. These +are different graphs. Folded descendants travel with a moved visual parent, +but selecting parent+child must not add a second delta. Selected port owners +already carry their ports and relative anchor/label data; do not translate that +data again. Internal routes translate once; one-ended routes keep world pins; +automatic routes reroute. Reuse State boundary rules without broadening their +meaning. Cache keys must include the captured selection/plan as well as delta: +the existing singleton preview-position cache key is insufficient. + +## Allocation decision + +Prefer the parent's proposed shared-destination, all-or-nothing group allocation +over always-arrange multi-selection. Shift changes cardinality, not drag meaning; +silently disabling reassignment only for groups would be unpredictable. + +Normal Allocation drag of a work-only selection should normalize semantic roots +and all leaves once, evaluate one frozen performer/outside destination for the +entire set, and preserve semantic ownership. Every constituent subtree must +satisfy existing role, mixed-allocation and geometric-coherence rules; one +refusal rejects the whole operation. Separate valid roots starting under different +performers are not inherently ineligible for an explicit common target. Never +weaken role-exit or collateral-change protections to make a group succeed. + +Exclude all moving placements from frozen drop occlusion. Allocate new relation +UUIDs once per leaf, reuse them on target revisits, and commit precisely the +previewed candidate through one Document transaction and one Undo step. Layout +must preserve group spacing while respecting the destination's constraints; an +impossible placement refuses the group rather than packing a subset. + +Arrange only uses the pure simultaneous geometry planner and preserves every +allocation record. A mixed performer/work/other set cannot promise semantic +reassignment: refuse a normal work-primary drag with actionable Arrange only +guidance, or offer an explicit group Move action. This remains a dependency of +full #29 acceptance even if geometry support lands first. + +## Other coherent actions and lifecycle + +Group Hide removes exactly the selected placements from the active diagram, +preserving semantic elements and other diagrams. Do not reuse movement's +descendant closure: hiding a parent must not silently hide unselected children. +Ports/edges without placements must not fall through from Hide to model Delete. +Group Delete from model needs one combined impact preview over unique semantic +targets, with overlapping descendants/incidents counted once. Avoid sequential +deletes of a parent followed by an already-removed child. One invalid/read-only +member rejects the whole command before any mutation. + +Fold/Unfold should apply one explicit state to all eligible selected containers, +not toggle each member into the opposite mixed state. Select All visible cards, +clear selection, nudge, and align/distribute are useful discoverable related +actions; alignment is geometry-only and not automatically suitable for derived +Behavior layouts. Duplication and bulk semantic-property edits need separate +contracts. Existing menus/help should advertise Shift+click, group count, +Arrange only, Hide versus Delete, and unavailable-action reasons. + +Selection-only changes create no model/source/history entries. Changing the +primary must not discard an unfinished inspector draft. A group gesture requires +clean admitted input; do not auto-apply an unrelated draft merely to start or +finish it. Dirty/read-only/pending/modal contexts keep their current input owner. +Opening a modal cancels drag authority but can retain visible selection; closing +it must not revive that drag. Source apply, Undo/Redo and property changes prune +vanished/invisible members while retaining survivors. Explicit UUID edits need +known identity remapping or safe pruning, never alias/name matching. Switching +document/diagram/layer/owner clears the active set. Native pointer ordering and +press/release batching must remain respected. Successful moves retain the group; +Hide/Delete prune it. Automatic reselection after Undo of deletion is a separate +UX decision, not an implied model-history guarantee. + +## Falsifiers before acceptance + +1. Shift held in a clean inspector before clicking the second function adds it; + no property text, model, source or history changes. Shift-click a selected + route/port handle does not start another editing gesture. +2. A parent and its child, plus an unrelated sibling, move once by one delta; + folded descendants and both-ended custom pins follow once. One-ended pins, + attachments, label offsets, allocations and other diagrams remain exact. +3. Two unrelated selected function endpoints move with their internal route; + preview projection/routing equals committed projection/routing. Undo/Redo is + one exact operation; zero displacement adds no history. +4. Allocation of two valid roots to one performer succeeds atomically. Add one + role-exit/collateral/ineligible member: candidate and release both refuse + without partial allocation. Arrange only moves the same set with allocation + records byte-for-byte unchanged. +5. During a drag, change selection, dirty source, open a modal, lose focus, + resize/pan/zoom, replace the document with the same project UUID, or deliver + an old release after a new press. No stale operation commits or cancels a + newer owner's gesture. Esc preserves model/source/history and the selection. +6. Group Hide preserves semantic records and unselected child placements; + group Delete previews the union once and preserves unrelated counterparts. + Mixed nonplacement/read-only selection never becomes partial success. +7. Group movement and ordinary primary-property Apply retain surviving members; + inspector clearly edits only the primary. Right-click a selected card keeps + group actions; text-field Delete/Ctrl+A remain text actions. + +These are proposed evaluators, not reported test passes. No edits to peer-owned +#16 resize or #31 traceability/navigation files are recommended by this note; +coordinate any necessary shared integration through their owners. + +## Review of the parent synthesis + +Reviewed `multiselect-contract-20260923.md` after its initial synthesis. Its +common selection plus family-specific planners, simultaneous geometry, +all-or-nothing Allocation, derived-order adapters and aggregate removal form a +coherent scope. The architecture planner is a useful foundation and does not +by itself establish full issue acceptance. No production implementation was +reviewed or executed in this pass. + +The selected-member stationary-release policy is a reasonable choice. My +initial preference for retaining the group favored easy primary inspection; +collapse favors the ordinary single-target click convention. Neither needs a +second preference setting. The chosen policy needs these precise boundaries: + +- Collapse only after a valid stationary press/release which never exceeded the + drag threshold. A drag which returns to its origin or snaps to zero has still + been a drag. A rejected destination, Escape, stale release, focus loss, or + modal entry must not reinterpret a group gesture as a singleton click. +- Freeze selection and primary for gesture authority. Refreshing the inspector + on selected-member press may change canvas bounds and cancel the gesture + through the existing geometry guards. Defer that refresh until the outcome, + or prove that a deliberately frozen pane arrangement preserves the captured + geometry. Do not weaken cancellation guards merely to mask this interaction. +- Specify whether the grabbed member becomes primary after a completed group + drag; either choice is viable, but the inspector and primary highlight must + agree while the full group remains selected. + +Two capability details remain material implementation decisions: + +1. Exact visual targets include ports, edges and read-only occurrences, while + the geometry planner consumes placements. The adapter must evaluate the + complete selection before calling it. For a mixed unsupported group, refuse + with an explicit reason; do not silently filter to placements, promote a + selected port to its owner, or infer an edge's endpoints as selected nodes. + A separately named nodes-only action could be specified later, but cannot + be implicit in the ordinary group drag. Arrange only has the same admission + obligation even though it preserves allocations. +2. Context Add/Remove must capture an exact target and retain the previous set. + The current singleton-select-before-menu pattern cannot run before Add or + the existing set has already been lost. Right-click a selected member should + retain its group for group actions; offering Add for an unselected target + requires retaining the prior set until that explicit command is resolved. + Therefore the blanket rule that right-click elsewhere first replaces the + selection needs an exception or an explicit two-stage menu contract. Menu + dismissal must not accidentally apply Add, Remove, or a singleton mutation. + +Additional falsifiers for the final policy: press a selected member, cross the +threshold, return to origin and release (group retained, no history); drag to +an invalid allocation target (group retained, explanation, no edits); open a +context menu on an unselected function and choose Add (old members preserved); +then remove the primary (latest surviving member becomes primary). Repeat the +first press from an inspector with different field height and confirm that +inspector refresh cannot turn the gesture into a stale drop. A mixed node/port +selection must receive the declared refusal rather than a partial move. diff --git a/harness/multiselect-behavior-design-20260923.md b/harness/multiselect-behavior-design-20260923.md new file mode 100644 index 00000000..8586e161 --- /dev/null +++ b/harness/multiselect-behavior-design-20260923.md @@ -0,0 +1,61 @@ +# Behavior multi-selection design review — 2026-09-23 + +Read-only review of published `b8685416f56e53e30bd11ecf93a54cea10ac8458` for issue #29. This note is a local-only proposal until the owning session publishes it. No runtime, build, GUI, or acceptance claim is made. Process, Scenario, and State are all within the requested usability outcome; architecture-only support is not equivalent completion. + +## Verified implementation boundaries + +| Surface | Identity and current gesture | Source at reviewed commit | +| --- | --- | --- | +| Process | Local occurrence/relation UUID; expanded reusable contents additionally require `OccurrenceEndpoint { references, occurrence }` and owner. Dragging the selected handle changes `steps` order only within one derived graph column. Links determine columns. | `crates/oa-desktop/src/behavior/topology/direct.rs:5`, `:29`, `:112`, `:448`, `:721`; `crates/oa-desktop/src/app/process_canvas.rs:145` | +| Scenario | Lifeline/occurrence UUID under a scenario; occurrence rows also identify their immediate sequence/operand parent. Drag changes semantic ordering, constrained to the same parent; the candidate is validated. A stationary row click selects on release. | `crates/oa-desktop/src/behavior.rs:1171`, `:1217`, `:1909`, `:1985`; `crates/oa-desktop/src/behavior/scenario.rs:1091` | +| State | Semantic UUID and exact placement UUID are distinct. Vertex movement mutates persisted geometry and includes projected descendants. State-action rows and transitions are relation UUIDs. | `crates/oa-desktop/src/behavior/state_canvas.rs:136`, `:983`, `:1359`; `crates/oa-desktop/src/behavior/state_edit.rs:131`, `:533` | +| Navigation | Existing `Surface`/`Target` describes saved placements, state rows, qualified process occurrences, preview links and scenario objects. Its navigation highlight is explicitly not an editing target. Coordinate with #31 before touching its files. | `crates/oa-desktop/src/diagram_usage.rs:105–156` | + +The shared viewport admits keyboard events only while focused, keyboard-enabled, and outside pan/space gestures (`behavior/canvas_workspace/viewport.rs:327`). `keyboard_owner_enabled` excludes modal menus, confirmation, reference inputs, workflow, and usage chooser (`behavior/canvas_workspace.rs:17`). Dirty inspector/source drafts and pending behavior editors add mutation guards (`app/diagram_removal.rs:24`; `behavior/state_view.rs:178`). Pointer state already captures document, owner/diagram, revision and epoch, and cancels on relevant context/view changes. Preserve these lifetimes. + +Delete currently has materially different meanings: persisted placement Delete hides that placement; Shift+Delete targets its semantic object. Derived Process/Scenario Delete removes the local semantic occurrence or relation, preserving reusable definitions. Qualified Process preview contents cannot be deleted from the caller (`app/diagram_removal.rs:55–110`, `:170`; `app/process_canvas.rs:96–113`). This distinction must remain visible in any group action. + +## Recommended minimal coherent contract (proposal) + +Use a common selection interaction and family-specific command planners. Do not add free geometry to derived Process/Scenario diagrams as an accidental consequence of this feature: doing so would require a separate persisted-layout specification and source/API parity decision. + +1. Selection belongs to `(document identity, surface identity)`, contains typed visual targets in stable order, and has one primary target for the existing inspector. Semantic UUID alone is insufficient for saved placements or repeated Process previews. A selected target may be inspectable without being movable/deletable. Invalid or newly hidden targets are pruned when the document/view changes; navigation to another owner clears selection (Back may deliberately restore a separately captured selection). +2. Plain click an unselected target replaces the set. Shift+click toggles membership; adding makes it primary, removing the primary selects the most recent remaining member. Track modifiers in the focused input path, not through a global shortcut that also fires in text fields. Shift selection must precede endpoint/bend/reorder handles and must never reconnect or start a move. +3. Press on a selected movable target preserves the set so drag moves the group. A stationary plain release may collapse to that target; decide once across families and test it. Right-click a selected member preserves the group; right-click an unselected item makes it the sole member before presenting commands. A stationary background click clears selection. Existing background pan must not clear it merely because the pan started. +4. Paint every member, distinguish the primary, and show a concise count and available group action. Single-object property editing remains explicitly the primary object's editor. Do not imply that changing its name, type, guard, or operand edits the set. +5. Group operations capture the complete immutable target set, primary, document, surface, revision and gesture epoch. Preview and commit use that same snapshot. One successful gesture is one document transaction/undo entry; Escape, stale context, modal entry, tool switch, focus loss, invalid release, or validation failure produces no partial edit. Preserve selection after successful mutation and reconcile after undo/redo. +6. Capability admission evaluates the entire selection, including qualified/read-only members. Unsupported mixtures must give a specific reason; silently acting on only the primary or silently omitting unsupported members is not a group action. If a supported action intentionally affects only nodes in a nodes-and-edges set, label it "Move selected nodes" and derive attached routes; never move a selected edge's endpoints implicitly. + +## Family-specific group operations (proposal) + +**State/saved geometry:** Translate all selected root placements by one world-space delta. Expand the union of each root's descendant placements from the original model; parent plus selected child moves the child exactly once, including folded descendants. Preserve semantic ownership and all relative offsets. Compute route translation against the complete placement-delta union once. `Command::Batch` containing independent `MoveNode` calls is not sufficient: each `MoveNode` currently computes its own route delta map (`oa-core/src/session.rs:771–806`), so a route between two independently moved roots does not receive the same treatment as one whose endpoints move in one operation. Prefer a small shared core group-move command/planner used by both saved architecture and State. Coordinate geometry interaction with #16; group resizing is not implied by group movement. + +**Process:** Reorder a selected group of local contributions as a stable block within their existing derived column. Retain their prior `steps` order, remove all selected IDs, and insert before the chosen unselected contribution. Drop inside the selected block is a no-op. Require one owner and one column for all selected contributions; reject qualified previews, links, and cross-column sets for this operation. Preserve all sequence/contextual link UUIDs, endpoints and guards. Presentation order must not become new topology. Explicitly distinguish the legacy ordered-path representation, whose `steps` order can carry behavioral meaning; the planner must validate the complete candidate and guidance should say "Reorder contributions", not "Move freely". Add block movement through both pointer and bounded earlier/later controls so small Fit views remain usable. + +**Scenario:** Reorder selected occurrences as a stable block inside one immediate sequence/operand parent, with the same remove-then-insert algorithm and no-op rule. Lifelines are not occurrence rows and cannot participate in this operation. Nested fragments/branches move with their owned contents; do not separately reorder a selected descendant when its ancestor is moved. Cross-parent selection may remain useful for inspection/deletion but cannot silently migrate between operands. Run existing full-candidate validation once after the proposed order, preserving call/reply, activation, lifecycle, and fragment semantics. Invalid causal order returns an actionable reason and retains the original sequence. Do not claim pixel-distance preservation or arbitrary x/y movement in this temporal view. + +**Bulk removal across families:** Reuse the current semantics with an aggregate target/impact preview and one atomic transaction. Deduplicate objects and descendants so overlapping selections are not applied twice. For persisted diagrams, expose "Hide selected placements" separately from "Delete selected model objects"; never silently delete an edge semantically in an otherwise placement-only hide action. For Process/Scenario expose "Delete selected occurrences/relations" and preserve definitions. Read-only qualified preview contents require opening the definition; selecting the outer reuse occurrence allows deleting that use. The preview must enumerate actual dependency effects (e.g. deleting a participant clears occurrence participant references; deleting a fragment removes its owned branches/steps) rather than merely displaying a count. + +Useful later group actions discovered from these workflows are hide/show placement sets, align/distribute geometry-only nodes, reorder block earlier/later, duplicate an occurrence block with identity/reference remapping, and applying genuinely shared editable properties. Alignment is not meaningful for derived temporal/topology layouts. Duplication and batch property editing each need their own typed semantic contract; do not expose existing singleton actions under a misleading multi-selection label. + +## Alternatives and staged dependencies + +- **Uniform free drag everywhere:** visually simple but breaks the present Process topology/Scenario temporal representation or requires new persisted layout models. Reject for this change unless explicitly undertaking that broader semantics work. +- **Geometry surfaces only:** quickest, but leaves the user's family-wide inability to select/act on groups unresolved. A staged delivery can name this limitation; it cannot close the full outcome. +- **Common selection plus capability planners:** recommended. Reuses transaction, identity and input guards while respecting derived layouts. Costs a small shared selection layer plus independently testable State, Process and Scenario planners. + +Implementation dependency order: (1) selection identity/primary/context reducer and gestures; (2) union-based core geometry movement and State/saved adapters; independently, (3) stable-block reorder planners and Process/Scenario adapters; (4) aggregate removal with impact/confirmation lifetime; (5) realistic OA/SA native validation, undo/save/reopen/source parity, fresh review. Sharing selection does not require merging navigation-only `Focus` with mutable selection or generalizing every diagram renderer. + +Choices to settle explicitly: stationary click on an already selected member retains group versus collapses on release; whether Shift+click toggles or only adds (toggle recommended); exact shortcut for Select All when canvas owns focus; whether marquee replaces existing blank-left-drag panning (retain current pan and defer marquee unless a deliberate cross-family gesture decision is made); whether bulk deletion is delivered in the same milestone or left as a specifically tracked gap. None requires changing semantic ownership through a drag. + +## High-risk falsifiers / acceptance examples + +- Select State parent plus child plus an independent node with an edge between roots. Move once: each placement and eligible authored bend translates once; other diagram placements and semantic parents are byte-equivalent. Undo/redo and source round-trip restore exact geometry. +- Reuse one subprocess twice and Shift-select the same inner occurrence in both previews. Highlights remain distinct; group deletion/reorder cannot mutate the reusable definition or confuse the two callers. +- Select noncontiguous Process contributions in one column, reorder together, and compare every link and guard. Cross-column drop rejects with no partial change. Exercise a legacy exchange-only ordered process as well. +- Select Scenario call/reply together and reorder; also move reply alone before call, across an operand, or across create/delete lifecycle boundaries. Valid block succeeds once; invalid candidate leaves sequence/model/source/history unchanged. Fragment plus descendant never duplicates or reparents a step. +- Shift-click selected transition endpoint, bend handle, State action row, Scenario message line, and Process reorder handle. Membership changes without starting reconnect/bend/reorder gestures. An ordinary drag still performs the established operation when selection is singular. +- Focus inspector text, open modal/usage chooser, switch document with same project UUID, change owner, scroll/zoom, lose window focus, or start source edits during group gesture. No queued release/Delete can commit against stale context; typed text shortcuts remain local. +- Select mixed nodes/edges or local/qualified-preview items. UI tells the truth about action capability and never silently operates on the primary alone. All visible selection states remain distinguishable without hue alone. + +No code edits were made as part of this review. These cases are proposed evaluators, not passing test results. diff --git a/harness/multiselect-contract-20260923.md b/harness/multiselect-contract-20260923.md new file mode 100644 index 00000000..8577aed6 --- /dev/null +++ b/harness/multiselect-contract-20260923.md @@ -0,0 +1,118 @@ +# Multiple selection and group actions: implementation contract + +User requirement: [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29) +asks for Shift+click selection of several functions, moving them together, and +related useful group actions. This contract derives from that request and the +existing graphical/textual equivalence requirement. It is a development decision, +not a claim that the feature is implemented or accepted. Inspected source is +`b8685416f56e53e30bd11ecf93a54cea10ac8458`; integration with later peer branches +requires inspection of their actual changes. + +## Selection and operation identity + +Selection is transient and scoped to one document and rendered surface. It has +an ordered set of exact visual targets and one primary target for the inspector. +A semantic UUID alone cannot identify a placed node, a port on a particular +placement, or a repeated occurrence in an expanded reusable Process. Navigation +highlights are not edit targets. Changing surface clears the set; accepted model +changes reconcile surviving exact targets without mapping a missing occurrence +to another occurrence of its semantic definition. + +Shift+click toggles the hit target. Plain click on an unselected target replaces +the set. Pressing a selected member retains the set for a possible group drag; +a stationary release that never crossed the drag threshold selects that member +alone. A threshold-crossing drag keeps the group, including cancellation, refusal, +snap-to-zero or returning to the original pointer position. Defer any primary +inspector/layout change until the gesture outcome. The interaction must use +captured pointer order and original gesture +geometry even if the inspector changes layout. Modifiers must be available on +the first canvas press after focus comes from another editor; passive modifier +tracking must not grant keyboard command focus. Escape cancels a live gesture +before clearing selection. Contextual Add/Remove from selection provides an +explicit alternative to the modifier gesture. Opening a context menu captures +its exact target without replacing the selection; Add/Remove operates on that +captured target. Dismissing the menu preserves the set. A visible count and appropriate +group actions distinguish group selection from the primary inspector target. + +Property edits continue to address the primary object. No implicit bulk property +editing is introduced. Group actions explicitly address the captured set and +must reject stale document, revision, surface or target identity before any +selection, source, history or allocation state changes. + +## Movement depends on the diagram's meaning + +Saved Architecture and State views store placement geometry. Translate the +union of selected placements and their visual descendants by one displacement; +each placement moves once, including when both parent and child are selected. +Derive all affected routes from the original diagram and the complete movement +map. Both moved endpoint owners translate internal pins; an unmoved endpoint +keeps world pins fixed. Existing State boundary rules still apply. Labels and +manual attachment fractions remain relative. Other views, semantic ownership, +contracts, IDs and unrelated records remain exact. + +Movement admission evaluates the complete typed selection. Unsupported mixtures +of placements, ports, edges, rows or read-only occurrences are refused with a +reason; adapters must not silently omit members or promote ports to owner nodes. + +Rejected approach: sequential `MoveNode` commands. Each command sees only its +own root/descendants, so routes between separate selected roots do not receive +the simultaneous translation required by `docs/language.rst`. A pure planner +can emit the existing atomic `Batch(EditNode..., SetEdgeRoute...)`; a new wire +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 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. + +In Allocation views, multiple selection must not silently change work-body drag +from assignment to arrangement. Work-only groups use one frozen common performer +destination and one all-or-nothing allocation plan. Normalize semantic work +ancestry, deduplicate leaves and placements, and apply existing role/coherence +rules to every constituent. Different valid initial performers on independent +roots are not automatically an ambiguous destination. Any invalid constituent +refuses the whole action. Mixed selections cannot partially assign only eligible +members; explain the refusal and offer the existing Arrange-only operation. +Explicit Arrange-only always preserves every allocation record. Destination +feedback and hit geometry remain stable throughout the gesture. + +## Related group actions + +Move/reorder and removal are the initial useful operation families. Saved-view +Hide removes selected placements rather than the underlying engineering objects. +Delete-from-model uses one aggregate impact review and transaction; preserve +opposite-layer counterparts under the established deletion contract. Process +and Scenario removal identifies local occurrences/messages/operands, not the +reused definitions. A mixed set that cannot express one clear removal operation +is refused with an explanation. Never act on only the primary object while the +interface depicts a group. Alignment, distribution and duplication are separate +capabilities to assess after this foundation; no speculative semantics are adopted. + +## Acceptance and dependencies + +1. Prove simultaneous geometry planning with unrelated roots/internal routes, + nested and folded selections, one-ended routes, typed ports and State + boundaries. Whole-model comparison, JSON/source round trips, atomic refusal, + one Undo/Redo and zero-motion history behavior are required. +2. Implement exact selection/primary state and focus-aware input. Exercise + Shift held before canvas focus, toggle/removal, stationary click versus drag, + captured event batches, hidden/stale targets and navigation invalidation. +3. Connect ordinary/State previews and commits to the geometry planner, then + implement group Allocation and Process/Scenario order adapters with their + independent semantic evaluators. Preserve current route/attachment cancel, + pan, read-only and pending-source/property behavior. +4. Implement aggregate removal admission, feedback and history. Destructive + semantics remain explicit; raw drafts must not be silently discarded. +5. Build and exercise realistic native workflows, including multi-function + selection/movement, nested selection, cancellation, source inspection, + save/reopen and one-step history. Review interaction and saved-model evidence + independently. Integrate with the release owner's actual combined source. + +The [Architecture critique](multiselect-architecture-review-20260923.md) and +[Behavior investigation](multiselect-behavior-design-20260923.md) contain source +references and additional falsifiers. Live ownership, implementation sequencing +and blockers belong to issue 29. Linux verification remains user-deferred. diff --git a/harness/multiselect-core-review-20260923.md b/harness/multiselect-core-review-20260923.md new file mode 100644 index 00000000..d1661ea7 --- /dev/null +++ b/harness/multiselect-core-review-20260923.md @@ -0,0 +1,37 @@ +# Independent simultaneous-movement core review — 2026-09-23 + +Scope: frozen local `crates/oa-core/src/diagram_edit.rs`, `tests/group_movement.rs`, and `lib.rs` export, against `harness/multiselect-contract-20260923.md` and base `b8685416f56e53e30bd11ecf93a54cea10ac8458`. This is a static correctness/design review, not a passing build, test execution, GUI or final-feature acceptance claim. No production files were edited and no Cargo command ran. + +Reviewed file SHA-256 values: + +- `diagram_edit.rs`: `7F307D64FE09947973871A05BC8D5DCA82B64B65A8E6B3AAED22D6EFC6497962` +- `group_movement.rs`: `BADEC10713B9E9961330A1A77710B6D0D75C27B482281B02BEA6868C52B8F994` + +## Findings, in priority order + +**No verified blocking correctness defect in the pure geometry planner.** There is one concrete scaling concern and two nonblocking coverage extensions. The latter are not claims that the implementation currently behaves incorrectly. + +1. **P2 — applying the emitted batch performs a full project-identity enumeration for every moved placement.** `diagram_edit.rs:74` emits one `EditNode` per placement. Its existing handler (`session.rs:530–544`) calls `reidentify_tracked`, and `reidentify` invokes `Project::contains` before the same-ID early return (`session.rs:873–879`). `Project::contains` builds the entire `identities()` vector (`model.rs:1761–1781`). Thus applying a group of M placements adds O(M × P) identity enumeration/allocation for P project records, although every placement retains its ID. This is a verified code-path cost, not a measured latency defect. It matters especially if a future preview applies the whole command on every pointer event. The planner itself computes parents once and has no repeated full-project clone. Recommended follow-up: measure representative large nested selections before choosing the preview path; if needed, skip identity remapping for an unchanged placement UUID after the handler's existing-node check, or apply validated geometry records through a preview path without full session execution. Do not widen this into unrelated transaction refactoring. + +2. **P3 — new-planner tests do not directly exercise projection-derived ancestry differing from semantic ancestry.** Current nested coverage uses an Activity child (`group_movement.rs:27`, `:148`). `projection::parents` specifically replaces work ancestry with allocation grouping and makes Traceability placements flat (`projection.rs:94–138`). The planner correctly calls that authoritative helper, so no divergent implementation was found. A compact follow-up fixture should select an allocated performer plus a selected work descendant and assert all visually grouped work moves exactly once without changing allocations; an equivalent Traceability fixture should assert an unselected semantic child remains stationary. Existing `release_routing.rs:403–494` covers portions of Allocation grouping through old `MoveNode` and route helpers, but not the new command planner's final whole-model result. + +3. **P3 — the new end-to-end preservation fixture uses default attachment/port-anchor data.** The route fixture always has `source_attachment: None` and `target_attachment: None` (`group_movement.rs:7–11`); the typed-port case has no authored `port_anchors` (`:221`). Clone-and-change implementation preserves them by inspection, and label metadata is already exercised. Populate at least one manual edge attachment and one owner-port anchor, then retain the whole-project equality and source round-trip assertion. This would make the contract's authored attachment preservation observable rather than relying exclusively on inspection. + +## Correctness traced to authoritative behavior + +- Admission checks the diagram, nonempty set, every exact placement membership, and finite delta before the zero-motion return (`diagram_edit.rs:18–36`). A foreign or missing selected placement cannot disappear into a partial move, including zero delta. Typed visual-target admission, visibility and stale document/revision checks remain adapter responsibilities; the helper intentionally receives placement IDs only. +- The closure in `:38–50` traverses `projection::parents` with a deduplicating visited set. It is equivalent to the existing `descendant_nodes` ancestry relation for accepted project snapshots, includes folded placements because folding is not used to filter `parents`, and cannot double-translate overlapping selected roots. It correctly preserves Traceability's flat projection and Allocation's visual parenting by delegation. +- Route derivation in `:51–52` uses the original view and the complete union of displacement entries. `translated_routes` resolves actual endpoint owner placements, including ports, requires both owners to move by equal delta, handles State Entry/Exit containing boundaries, and clones changed routes (`projection.rs:169–224`; `projection/state.rs:119–144`). One-ended routes remain exact; label offsets and attachment fractions are retained. This addresses the independent-root failure of sequential `MoveNode` calls. +- Each placement clone changes only x/y (`diagram_edit.rs:55–78`). Bounds match existing validation: finite values within `MAX_COORDINATE`, positive bounded dimensions (`validation.rs:820–840`). Translated route pins are checked before returning a batch. Partial command construction cannot mutate the immutable input or escape on error. +- `EditNode` with the unchanged ID retains provenance; `SetEdgeRoute` updates its existing entry in place, retaining unrelated route ordering (`session.rs:749–755`). Batch execution prepares one candidate, validates the final state, and publishes one history entry only if the candidate changed (`session.rs:172–198`, `:250–269`, `:386–395`). The planner's returned command-selection ID is the last changed record rather than the user's primary; its doc comment correctly warns adapters not to adopt it as primary selection. +- Desktop `Document::execute` performs source update before committing (`document.rs:322–335`), so using this existing command path preserves model/source atomicity. The planner does not itself enforce source-draft or focus guards and should not be treated as doing so. Existing `Document::commit` suppresses unchanged source bundles (`:399–406`); the new zero-motion test directly proves only the core Session contract, not a desktop gesture/source-draft integration. + +## Test review and evidence limits + +The main sibling test (`group_movement.rs:71`) independently assigns three exact expected positions and the internal route pin, then compares the entire Project. It proves intent independently of `translate_placements`, rather than comparing a preview and commit that share the same bug. It also includes a useful negative control showing sequential old moves retain the original internal pin, one Undo/Redo, JSON round-trip, and source-update/recompile preservation with a comment. The typed-port test independently constructs its complete expected Project (`:221`). + +The folded-overlap test partly compares two planner outputs, but also independently asserts the hidden child's exact position and unchanged routes (`:148`). The invalid-input test covers unknown diagram, empty/mixed/missing membership, nonfinite/overflow deltas, translated route overflow, no mutation, and zero history (`:183`). The State boundary test checks independent expected route coordinates when the boundary moves and unchanged pins when it does not (`:269`); it is intentionally narrower than a whole-project comparison. + +The input immutability assertions are inherently weak by themselves because the helper takes `&Project`; their value is secondary to the complete expected-result assertions. No test-run result was inferred from reading test source. No GUI adapter, mixed target-set admission, allocation reassignment, Process/Scenario reorder, stale input, preview latency, or full feature completion was reviewed or accepted here. + +Overall: the frozen foundation implements the selected pure-geometry contract coherently. Retain the pending performance check and targeted coverage extensions while proceeding to adapter work; neither a successful compile nor this review alone establishes interaction acceptance. diff --git a/harness/multiselect-desktop-review-20260923.md b/harness/multiselect-desktop-review-20260923.md new file mode 100644 index 00000000..ef12a696 --- /dev/null +++ b/harness/multiselect-desktop-review-20260923.md @@ -0,0 +1,62 @@ +# Independent ordinary saved-view multi-selection review — 2026-09-23 + +Read-only review of the in-progress ordinary saved-view adapter against `multiselect-contract-20260923.md`. The owning session was actively wiring UI during inspection; references describe the inspected code and must be rechecked after fixes. No production edits, Cargo, GUI, or runtime/test acceptance claims. Missing State, Allocation, derived Process/Scenario and aggregate model-delete adapters are explicitly outside this review, not findings below. + +Reviewed primary files: `crates/oa-desktop/src/app/multiselect.rs`, `diagram/multiselect.rs`, `diagram/group_gesture.rs`, `diagram_selection.rs`, plus relevant `app.rs`, `diagram.rs`, `diagram/focus.rs`, `views.rs` and menu paths. + +## Verified findings + +### P2 — surviving selection is discarded when legacy sync removes the primary + +`app/multiselect.rs:78–97` decides whether the group held the primary by comparing the old selection against **post-sync** `selected_node`/`selected`. But `sync_with_alias_editor` clears nonexistent semantic selection and calls `refresh_draft`, which recomputes/clears the placement (`app.rs:800–810`, `:829–853`). Consequently the reducer's correct survivor fallback is overwritten. + +Concrete path: select placement A and B, with B primary; undo the model operation that created B, or remove B's placement through a singleton-target action. `sync` first makes `selected_node = None` (and may select the project). At final reconciliation, `held_primary` is false. `Selection::reconcile` prunes B and chooses A, but `legacy_target` is None, so line 95 clears A too. The same problem occurs when an external accepted model/source change removes the primary while other exact selected placements remain. + +Preserve provenance of the pre-change primary across sync, and distinguish genuine explicit navigation from model/history reconciliation. The latter should prune only invalid members and inspect its surviving fallback. Do not recover by mapping an old semantic UUID to an arbitrary replacement placement. Regression: select A/B, undo B creation; expect A selected and inspected, no additional history. Repeat for placement deletion and semantic deletion, plus deletion of a non-primary member and ordinary group-move Undo/Redo. + +### P2 — mixed selection cannot collapse through the specified stationary click + +`diagram/multiselect.rs:83–97` tests whole-set movement capability on pointer press. For a legal selected set containing placement A and edge E (or a port), a plain press on either member immediately publishes `Refuse`, stores no click gesture, and captures the press. A stationary release therefore cannot execute the contract's "select that member alone" behavior. This is a selection defect inside ordinary saved views, not a request to implement mixed-target movement. + +Track a deferred click for every selected exact target, independently of movement capability. On a stationary release replace with that target; only crossing the drag threshold should refuse unsupported movement while retaining the set. Regression: A+E, click A, expect exactly A and no model/history mutation; recreate A+E and drag A, expect refusal and both retained. Repeat with a port and with pointer displacement that never exceeds the threshold. + +### P2 — a left press can start group movement while middle-button pan owns the pointer + +The new `group_input` runs before the existing singleton left-press branch (`diagram.rs:724` versus the middle-pan guard around `:858`). It has no `state.pan_start` admission check. Reproducible event path from inspection: select A/B, middle-button press sets `pan_start`; while holding middle, left-button press on selected A enters `GroupGesture::new`; subsequent cursor motion is consumed by the group and left release emits a model move even though the middle pan remains active. The legacy singleton handler explicitly captures this left press without moving nodes. + +Keep gesture ownership exclusive before starting or toggling group selection. Reject/defer left presses while middle pan is active, and ensure starting a group cannot coexist with `drag`, `route_drag`, `port_drag`, or work gesture state. Regression should replay middle-down → left-down on selected node → move → left-up → middle-up and assert no geometry/history/selection mutation attributable to a group drag. + +### P2 — Delete hides the group during an active drag + +The active-group dispatcher handles only cursor movement and left release (`diagram/multiselect.rs:27–45`). Delete falls through into the unconditional multi-selection Delete branch at `:54–63`, emitting `Hide(snapshot)` while the live preview exists. App validates that snapshot and hides the placements. By contrast the established singleton Delete branch requires every move/route/port/pan/work gesture to be idle (`diagram.rs:803–813`). Group selection should preserve this input-safety convention. + +Require no live pointer gesture before publishing Hide, or define and test an explicit cancel-then-delete policy. Do not silently delete while the drag's release is still pending. Regression: A/B selected, start group drag and move, press Delete before release; expect no deletion/model history, retain the selection, and verify the subsequent release follows the chosen policy. Cover Delete during middle pan too: the same new branch also lacks `pan_start` gating. + +## Performance observation (P3, no latency measurement) + +Every accepted `App::update` calls `reconcile_multiselect` (`app.rs:1383`), which unconditionally calls `projection::project_view` (`app/multiselect.rs:76`) while on Page::Diagram. That rebuilds `ViewIndex::new`, including the global BehaviorIndex/evidence and all edge facts (`oa-core/src/view_options.rs:484–543`). Thus each pan delta, inspector keystroke and unrelated asynchronous completion now incurs global semantic indexing plus projection, even when surface/model visibility and selection are unchanged. Explicit Choose additionally projects before calling reconciliation and again at end of update. + +Reuse `document.view_index()` via `project_view_with_index`, and separate revision/surface/visibility reconciliation from explicit legacy selection changes. Cache or skip the former when authority and visibility inputs have not changed. This is a verified unnecessary work path; responsiveness has not been measured and no timing claim is made. + +## Sound portions and boundaries + +- `Selection` stores insertion-ordered exact typed targets, maintains primary membership, prevents Whole admission, and uses a generation plus exact snapshot/context comparison to reject remove/re-add and primary ABA cycles. The reducer's survivor fallback itself is correct; the defect is App's later legacy reconciliation. +- `placements` collects an `Option>` over the full target set, so movement/hide cannot silently filter ports or edges. Selection events are checked before App's general allocation/camera boundaries, and operation events require the exact captured selection generation. +- `GroupGesture` freezes viewport and press geometry, consumes release position, latches threshold crossing, preserves a group after return-to-origin or snap-to-zero, and permanently retires invalid/outside pointers. Ordinary group input checks current selection snapshot and viewport before processing it. Preview applies only the geometry planner's emitted records, avoiding the full session-per-frame identity work identified in the core review. +- The existing ordered `state.pointer` normalization occurs before `group_input`, preserving native event-order handling rather than using the final batch cursor for earlier presses/releases. +- Escape, viewport/context invalidation, window focus loss, cursor leave, wheel, and right/middle press cancel an existing group before committing. App independently denies dirty drafts, busy/modal ownership, active architecture editors and connections. The newly passive modifier path is appropriate for Shift held before canvas focus; it does not grant keyboard command focus. +- Model/source transactions still flow through `App::execute` and `Document::execute`. Successful group movement restores the exact primary after the command's implementation-selected last record; zero movement returns before execution. Source failure is not accepted as a successful group operation. + +Future verification should use mounted App and native-ordered canvas-event tests for the four findings, then actual GUI exercise. Reducer and gesture unit tests alone cannot expose the cross-layer sync and competing-gesture paths above. Existing known incomplete adapters are neither accepted nor reopened by this review. + +## Follow-up inspection while desktop-1 gate runs + +The owner has addressed the reported paths in source. This status is static inspection only; the review findings above remain useful regression definitions and are not asserted to be outstanding in the updated source: + +- App reconciliation now detects an invalid exact primary before pruning and retains the reducer's survivor fallback when legacy sync has no target. +- A typed deferred `multiselect::Press` preserves mixed-selection stationary clicks; movement refusal occurs only after threshold crossing. +- An active group captures keyboard input, so Delete does not fall through to Hide; existing Escape cancellation still precedes that capture. +- Group admission yields to an existing `pan_start`, preserving the legacy middle-pan exclusion and Delete guard. +- App reconciliation is conditional on its selection/model/surface boundary and uses the document's cached view index. + +Eleven production-canvas event regression tests have been drafted in the local-only `.local/multiselect-canvas-tests.rs` for later authorized incorporation into `diagram/multiselect_tests.rs`. They exercise actual `DiagramCanvas::update`, including ordered native batches, captured status, preview records, snapshots and independent literal geometry expectations. Coverage includes Shift hit identity, 0.5/1/2 zoom grouped movement, mixed click versus drag, Escape, Delete during drag, middle-pan exclusivity, seven viewport/context changes, membership ABA, release-only displacement and return-to-origin. The parent owns App-level survivor tests, module registration, compilation and native verification. No draft test has been compiled or run by this reviewer; no production test file was created during the frozen-input gate. diff --git a/harness/multiselect-fresh-review-20260923.md b/harness/multiselect-fresh-review-20260923.md new file mode 100644 index 00000000..f4f0a44a --- /dev/null +++ b/harness/multiselect-fresh-review-20260923.md @@ -0,0 +1,133 @@ +# Fresh ordinary saved-view selection review — 2026-09-23 + +Independent static correctness review of the local, uncommitted feature on +`codex/multiselect-9e8r6t-20260923`, based on +`b8685416f56e53e30bd11ecf93a54cea10ac8458`. Scope is ordinary saved views only, +against [the implementation contract](multiselect-contract-20260923.md). +State, Allocation, Process/Scenario adapters and aggregate model deletion are +known incomplete work and are not findings in this review. The pure core +movement planner has its [separate review](multiselect-core-review-20260923.md). + +Reviewed the selection reducer, App adapter, canvas adapter and gesture, focus +owner, App/menu/view integration, selection and focus tests, and adjacent +selection/sync/route/navigation paths. The initial review did not edit production +or test source. The parent subsequently delegated the bounded correction below. +No test execution, builds, GUI operation, Gitea update or publication were +performed by this reviewer. + +## Findings + +### P2 — starting passive I/O loses Shift that was already held + +`crates/oa-desktop/src/diagram/focus.rs:93–97` replaces the wrapped canvas tree +with `Tree::new` when suspension starts. This intentionally retires pointer +gestures, but also resets the newly added `State.modifiers` +(`diagram.rs:69–72`) to its default. The only normal assignment of a nonempty +modifier state is receipt of `ModifiersChanged` (`diagram.rs:615–617`), and +the toggle decision reads that retained state (`diagram/multiselect.rs:208`). +The wrapping view supplies `app.busy` as suspension (`views.rs:511–516`). + +Concrete event path: select A; deliver `ModifiersChanged(SHIFT)` while a clean +Inspector field owns focus; start Save/export through a mouse action while +continuing to hold Shift; let busy become true and then false; click B without +an intervening modifier transition. The tree replacement discards Shift, so +the click follows the plain selection path and replaces A with B instead of +adding B. The same loss occurs if the canvas owned focus before I/O. No stale +model mutation is needed to trigger it. + +The existing test +`mounted_suspended_modifiers_are_passive_and_survive_until_resumed_click` +(`app/diagram_focus_tests.rs:329`) sends Shift **after** setting busy and +settling, so it verifies a different ordering and cannot catch this path. + +Retain passive modifier state across gesture retirement, or restore it from +an owner whose lifetime survives the canvas replacement before the first +resumed press. Keep the existing I/O pointer-retirement behavior. Add the +mounted replay with Shift delivered before suspension, no modifier events +during suspension/resumption, and assert A/B selection, unchanged model and +history, and no keyboard focus granted by modifier tracking. This finding is +established by the explicit state-reset path; runtime reproduction remains +for the owning session's affected gate. + +No other new in-scope correctness finding was established. + +## Rechecked protections + +- `Selection::matches` combines document/surface/revision/epoch, membership, + primary and monotonic generation. The App rejects stale operation envelopes + before allocation and camera boundary side effects. Choose/Clear use context + admission; movement, Hide, refusal and collapse require a captured snapshot. +- Full typed-set admission prevents silently dropping edges or ports from a + group movement/Hide request. Exact visibility is rechecked at execution. + Reconciliation now preserves surviving targets when the exact primary + disappears, and does not substitute a new placement of the same semantic + element. The Placement-properties identity correction is present in + `legacy_target`. +- A selected-member press defers primary/Inspector changes. Its stationary + release collapses even a mixed group; a threshold-crossing mixed drag + refuses while retaining membership. Threshold crossing remains latched for + cancellation, zero movement and return to origin. The latest + `Press::viewport_matches` delegates to the underlying gesture, so its retired + state cannot regain authority. +- Ordered pointer normalization precedes group handling. Group preview and + release consume the same captured geometry and planner. Active groups + consume keyboard input; Escape cancels before a subsequent Escape clears. + Middle pan excludes group admission. Context, selection generation, + viewport and window/pointer lifecycle changes retire the gesture. +- Context-menu opening/dismissal preserves membership; Add/Remove uses the + captured target, and selected-member group menus expose aggregate Hide and + explicit inspection/collapse. App guards prevent queued singleton Move, + diagram removal or model Delete from acting on one member of a new group. + The legacy Hide action redirects to the whole visible set. +- Ordinary passive modifiers are forwarded without acquiring keyboard focus, + including while another mounted field owns focus. Mounted tests cover this + primary requirement and unfocused command denial. The finding above concerns + the separate tree-replacement lifetime. +- The earlier unconditional global reconciliation cost is removed. Boundary + changes trigger reconciliation and projection reuses `document.view_index()`. + No latency or scale-performance measurement is claimed here. + +## Acceptance limits + +The parent reports desktop-3 passed 1,254 tests with zero failures and six +ignored on its recorded 729-file input set, followed by the small +`Press::viewport_matches` change. That report was context for this review; +this reviewer did not rerun or independently certify it. The parent owns the +affected gate, core/native checker result, native interaction verification and +integration acceptance. This static review cannot establish actual Windows +focus behavior, visual clarity, save/reopen usability, or performance. + +The review inspected `diagram/multiselect.rs` with SHA-256 +`C8ADF20046176F620CAEF52DD8ED93EEDB349F72D8C9F50401AAA440848803B4`, +`diagram/focus.rs` with +`16AA9C6254BE7E30C67A71C0D5FE5FC763F40923912154209907D0FA17E75889`, and +`app/multiselect.rs` with +`D3CA4A2D593CB15CEDD40897BBCCF410111D9E54AD8ADFE9AD985B47611B0B0B`. +These identify local reviewed contents, not a published commit. + +## Delegated correction after review + +The parent authorized edits only to `diagram/focus.rs`, +`app/diagram_focus_tests.rs` and this note. The focus owner now stores passive +modifier state independently of the child canvas and marks it for restoration +when suspension replaces the child. Before the next admitted child event it +replays `ModifiersChanged` without acquiring keyboard focus. A genuine modifier +change updates that retained state and window unfocus clears it. The existing +subtree replacement, pointer retirement, suspended-input rejection and keyboard +ownership checks are preserved. + +Two mounted regression tests were added in source: + +- `mounted_shift_held_before_suspension_survives_without_another_modifier_event` + holds Shift with Inspector focus before I/O, crosses busy/resume without a + modifier transition, and verifies that the next click selects A/B without + model/history/draft changes or passive focus acquisition. +- `mounted_shift_release_or_window_unfocus_during_suspension_does_not_restore_stale_shift` + covers release and window-unfocus while busy, then another suspension cycle + to prove the retained owner state was updated rather than restoring stale + Shift. The resumed click must select B alone with unchanged model/history. + +The correction and test source were statically inspected; the parent owns +compilation, execution and acceptance. The P2 above describes the pre-correction +path and remains the regression rationale, not a claim that the new source +still has that path. diff --git a/harness/multiselect-native-review-20260923.md b/harness/multiselect-native-review-20260923.md new file mode 100644 index 00000000..13d32b78 --- /dev/null +++ b/harness/multiselect-native-review-20260923.md @@ -0,0 +1,277 @@ +# Independent native evaluator review — 2026-09-23 + +Read-only review of `crates/oa-core/examples/multiselect_native.rs` at +`12a7352ddef5f69a144ead8219a50916233d63a9`, the local native preparation, +snapshot and capture helpers, and the owning session's proposed UI sequence. +Only this note was edited by the reviewer. No builds, tests, UI actions or +Gitea operations were run. The parent reports core, full desktop and lint gates +passed; no native run was available for acceptance at this review checkpoint. + +## Finding and correction + +**P2, corrected in the helper during review:** the original +`.local/multiselect-native/prepare-native.py:38–41` recorded the GUI exit code +and binary/fixture immutability booleans, then returned normal helper success +even if the GUI crashed or a recorded input changed. This could mislead a +consumer using process exit status as the gate. The parent added assertions at +lines 42–43 requiring GUI exit zero and all three immutability conditions after +writing the durable receipt. Re-reading the helper confirmed the correction. +No production source or build input changed for this correction, and it was +not runtime-tested by this reviewer. + +No additional actionable checker correctness defect was established. A native +acceptance result still requires the actual UI sequence and evidence below; +successful launcher completion alone is insufficient. + +## Independence and falsification coverage + +- The expected Project starts from the loaded baseline and changes explicitly + enumerated fixture records. Movement expects A, B and the child to move once, + translates only the internal route's pins, and preserves C, one-ended route + pins, attachments, labels, all semantic records, traces and other views. + Hide removes only A/B placements. It preserves the unselected child, stored + routes and model objects. Neither expectation invokes the group planner, + descendant traversal or routing implementation. +- `equal` compares the complete Project, not a selected geometry subset. + Validation, JSON roundtrip and source compilation/roundtrip supplement that + comparison. Completeness warnings in this deliberately partial fixture are + allowed; source discovery errors and compilation failures are rejected. +- The ten checker tests include positive generation/source/command examples + and multiple valid-model corruption falsifiers. The negative cases cover + an omitted or double-moved child, stranded internal pins, moved one-ended + pins, attachment/label drift, semantic and trace drift, other-view and fixed + node changes, invalid movement evidence, destructive Hide and unchanged-mode + semantic drift. Corruptions are validated before comparison and most tests + require the specific whole-Project mismatch, avoiding a fake pass caused + only by malformed input rejection. Planner use is confined to generating + positive/corrupted test candidates, not expected evaluator output. +- Fixture UUIDs are deterministic, generation refuses an existing output + directory, and evidence writes are exclusive. Preparation validates binary + hashes against successful unchanged-input build receipts and requires the + GUI and checker input-manifest hashes to match. The inspected checker + receipt names the reviewed commit; the GUI's then-current started receipt + records the same input hash. Final build/run results remain the parent's gate. +- The snapshot helper copies source into a unique directory, compares the + source file set and hashes before/after copying and after checking, pins the + checker and baseline, and propagates evaluator failure. Source snapshot + evaluation is separate from UI capture, as it should be. + +## Acceptance limits for the planned sequence + +The parent plans native context Add for A/B, actual drag, saved-model checks, +one-step Undo/Redo with exact source-byte comparison, undo back to baseline, +group Hide with Undo/Redo, and reopen. That is appropriate evidence for the +ordinary saved-view path with these explicit limits: + +1. **The movement checker is an invariant evaluator.** It infers the common + delta from saved A and checks bounds, nontrivial length and anchor grid + alignment. A wrong drag direction or magnitude can pass if all required + records share the same admissible delta. The code and manifest already + state this limit. Record native press/release and viewport observations if + claiming intended displacement accuracy; otherwise claim only the verified + common translation and preservation invariants. Existing pointer/gesture + tests provide separate evidence and are not native coordinate evidence. +2. **Model equality does not prove an action happened.** Capture one actual + Undo or Redo and completed Save between the appropriate snapshots. An + unchanged check alone cannot distinguish successful Undo from never making + an edit. Reopen must actually load the saved working source in the GUI and + demonstrate its state; checker roundtrip alone does not verify GUI reopening. +3. **Exact source-byte restoration is a separate comparison.** The snapshot + helper records each source inventory but does not compare different stages. + Explicitly compare all relative paths and hashes: Undo movement to the + original fixture source, Redo movement to the first moved snapshot, and + corresponding baseline/hidden inventories for Hide history. A Project PASS + does not establish identical source formatting or file layout. +4. **Join provenance rather than assuming it.** Tie the captured native window + to the launched PID/executable in `started.json`, identify the opened working + entry, retain the build/input receipts, and require the final fixture and + binary immutability checks. The checker verifies the expected fixture project + ID, not a baked-in baseline hash; baseline authenticity comes from the + generated fixture inventory and unchanged final receipt. Capture JSON and + screenshots alone do not establish which binary produced saved source. +5. **This fixture is OA Activities on an ordinary Overview.** Native context + Add verifies that explicit alternative, not held-Shift injection (unavailable + through the chosen native tool). Mounted focus tests separately exercise + Shift before canvas focus. The native run does not establish an SA Function + workflow, selected ancestor-plus-child deduplication, mixed-port/edge refusal, + busy suspension behavior or every cancellation path. Existing targeted tests + remain separate evidence for those cases; no additional broad native claims + should be inferred. +6. **Scope remains incomplete beyond ordinary saved views.** State, Allocation, + Process/Scenario and aggregate model deletion are not accepted by this + checker or planned native sequence. Their absence is not a new finding in + this bounded review. + +These limits qualify what the evidence proves. They do not require replacing +the independently enumerated Project oracle or broadening this native fixture +into the unfinished adapter families. + +## Native layout follow-up: selection changes move the canvas + +**P2 — group feedback changes the diagram viewport while merely selecting.** +The parent subsequently supplied native run-1 captures. I independently viewed +`.local/multiselect-native/run-1/06-select-a-0.jpg` and +`08-group-selected-0.jpg`; both capture the same native window at 1442 by 932. +The canvas begins near y=314 in the singleton capture and y=370 after forming +the group. A's header moves from approximately y=484 to y=511. The group count, +Hide/Clear actions and primary-properties explanation are readable, but adding +them has moved the diagram under the user's pointer. This is a presentation +finding; it does not assert that selecting changed saved geometry. + +The source cause is explicit: `views.rs:434–461` conditionally appends a two-row +group block above the View content control and canvas when the target count +exceeds one. The additional block is about 55 pixels high. The observed node +shift is smaller than the viewport-top shift, consistent with the canvas resize +and current fitted camera reacting to the reduced height. Removing the final +group member or clearing selection reverses the same conditional layout. + +The ordinary idle hint (`views.rs:654`) also mentions right-click, drag and zoom +but gives no visible Shift-click discovery cue; neither inspected screenshot +shows such a cue. Context Add is usable, but the user's requested modifier +gesture should be discoverable directly in the ordinary selection UI. + +### Smallest space-conscious correction + +Move the group feedback into an **always-mounted, fixed-height selection/status +strip at the existing fixed 30-pixel hint location** near `views.rs:658`. Remove +the conditional group block from the toolbar. A roughly 56-pixel strip can +contain: + +- A fixed approximately 32-pixel first row with selection count, the existing + group Hide/Clear controls when applicable, and the short explicit disclosure + `Properties edit only`. Use a bounded/nonwrapping primary label so + long names or a mixed selection cannot increase strip height. Empty and + singleton states keep the same row allocation even with fewer controls. +- A short second line: `Shift-click to add/remove · drag selected to move · + right-click for actions`. Existing active connection, allocation and route + instructions retain precedence when those modes own input; ordinary idle + selection should expose the modifier hint. + +This reuses the existing status location and consumes about 26 more vertical +pixels than today's singleton layout, while recovering about 29 pixels from +today's group layout. The shown central workspace is approximately 860 pixels +wide, sufficient for these compact controls and a bounded primary label without +adding a third row. A fixed outer height, fixed first-row height and nonwrapping +text keep the canvas rectangle identical for zero, one and multiple targets. +Keep this strip outside the canvas; an overlay would obscure diagram content +and complicate hit testing. Simply reserving the current extra 55-pixel toolbar +block would also stop the jump, but spends more permanent vertical space than +reusing the existing status strip. + +The narrow verification target is mounted canvas-bounds equality across +0→1→2→1→0 selection transitions, including mixed selection and a long primary +name, with unchanged model/history for those selection changes. A native +before/after capture should confirm stationary node screen positions and readable +controls at the same 1442-pixel window width. No source changes, build or native +interaction were performed by this reviewer for this follow-up. + +### Delegated layout correction + +The parent subsequently authorized edits to `views.rs`, +`app/diagram_focus_tests.rs` and this note. The conditional toolbar group block +is removed. Every active ordinary diagram now reserves a 56-pixel strip in the +former hint location with a fixed 32-pixel controls row. Count is always shown; +the existing group Hide/Clear intents and primary-only property explanation are +shown for groups. The primary name remains shortened and all strip text is +nonwrapping. Hide still requires whole-set placement admission and all App +event guards are unchanged. + +Idle selection hints expose Shift-click add/remove. Supported ordinary placement +groups advertise dragging selected elements together; mixed groups and Allocation +groups receive selection guidance without claiming a movement adapter they do +not have. Active allocation feedback and connection instructions retain priority; +singleton route/promoted-edge hints are preserved. The strip remains outside the +canvas with clipping, and no overlay or global layout policy was introduced. + +One mounted regression was added in source: +`mounted_selection_strip_keeps_canvas_and_fitted_camera_fixed_for_empty_single_group_and_mixed`. +It mounts the real App at 1442 by 932 with fitting enabled, a very long primary +name and a visible edge, then checks 0→1→2→3 mixed→2→1→0 selection transitions. +Each step must retain the exact measured canvas rectangle, zoom, pan, fit flag, +observed canvas size, complete Project, source bundle and empty history. +The previous implementation changes the canvas rectangle on the 1→2 transition, +so this verifies the observed regression through production layout and sensors. + +Only a static inspection and `git diff --check` were performed for these edits. +Formatting, compilation, regression execution and a fresh native before/after +capture remain owned by the parent. Native run-1 continues against its unchanged +pinned binary and cannot serve as verification of this subsequent source edit. + +### Native run-2 visual follow-up: stable selection strip + +I independently inspected the saved images `03-open-submitted-0.jpg`, +`05-singleton-settled-0.jpg` and `07-group-selected-0.jpg` under native run-2. +This is image inspection separate from the parent's live GUI operation; the +inspector also authored the layout correction, so it is not a fresh-author +review. No UI actions or source edits were performed for this check. +The run records source `f6a6010baed9e6164a1fcc7ecd58256a562827d7` and GUI SHA-256 +`49396ee5c24f749cbee54e1fd77e15f5d73b0e19cf246724216ce7464d3deba5`. + +At the recorded 1442 by 932 window size, all three images show the canvas top +near y=341 and the zoom at 84%. A's header remains near y=497, B near y=531, +the child near y=606 and C near y=656 across zero, one and two selected targets. +Selection outlines and the Inspector change; the diagram and canvas do not +visibly jump. This corrects the run-1 singleton/group viewport and node shifts +documented above. Pixel positions are approximate visual readings, not a new +instrumented layout measurement. + +The strip clearly shows `0 selected`, `1 selected` and `2 selected` respectively. +The group image displays readable `Hide selected`, `Clear selection` and +`Properties edit only B - assess request` on one line with space remaining. +The second line exposes Shift-click add/remove in each state, and the group +state additionally says to drag selected elements to move. No wrapping, +overlap, clipped action caption or obscured diagram content is visible in these +captures. No new visual correction is indicated for this shown window size. + +This follow-up establishes only the shown 0/1/2 selection layout and displayed +zoom stability. It does not yet claim run-2 movement, Hide, history, saved-model +checks or completed process lifetime; those actions were still pending at the +time of this image inspection. Long-name/mixed-selection and exact camera/model +invariants remain separate mounted-test evidence, not claims derived from these +three images. + +### Closed run-2 follow-up: Hide, history and reopening + +After run-2 closed, I independently viewed `15-before-hide-saved-0.jpg`, +`16-hide-group-0.jpg`, `18-hide-undo-0.jpg`, `20-hide-redo-0.jpg` and +`23-reopened-hidden-0.jpg`, and read the nine snapshot checker receipts, +`history-bytes.json` and `finished.json`. I also rehashed the actual nine source +files in each snapshot to compare the three history equality groups, and read +the saved moved/hidden OA view source. This remains an evidence inspection; +the parent observed the actual UI actions. It does not independently +authenticate individual key or pointer events or rerun the model compiler. + +Before Hide, two targets are selected. The hidden captures remove A/B while +retaining the child and C; the Explorer retains the engineering activities. +Undo restores A/B and their visible relationships, while the transient selection +appropriately remains empty. Redo returns to the same hidden visible state. +Across these four images, the canvas top stays near y=341, zoom remains 84%, +and child/C positions stay near y=606/y=656. Returning from two selected to zero +therefore no longer causes the run-1 viewport jump. The count, status text and +Inspector change without crowding or covering diagram content. No further +material visual defect was established in these captures. + +The reopened hidden image still has the same canvas allocation and only child/C +visible. Its zoom is 88% and the nodes have been refitted into the available +space. This is a reopened transient-camera change, not evidence of changed saved +placement geometry: the reopened source is byte-identical to the other hidden +snapshots and its complete-Project Hide check passes. + +All nine checker receipts report exit zero and PASS, with unchanged binary, +working source and baseline pins. The movement and movement-Redo reports infer +the same `(100, 60)` delta. The saved moved view explicitly contains A at +`(180, 140)`, B at `(570, 180)` and the child at `(220, 270)`; C remains at +`(780, 270)`. Internal pins translate, while the external pins and relative +route metadata stay unchanged. The hidden view retains both stored routes and +the baseline child/C placements while omitting A/B placements. + +My independent source-inventory hash comparison confirms the recorded byte +groups: baseline = movement Undo = before Hide = Hide Undo; movement = movement +Redo; hidden = Hide Redo = reopened hidden. Each snapshot contains nine files. +The history receipt reports that only `views/diagram_oa_view.arcadia` differs +from baseline for movement and Hide. The final lifecycle receipt records clean +exit zero, unchanged GUI/checker/fixture and empty application stdout/stderr. +There are 24 numbered run-2 capture pairs. These results support the stated +saved-state and visual relationships together with the parent's action +observations; the invariant-oracle, native Shift, SA and unfinished-adapter +limits documented above continue to apply. diff --git a/harness/multiselect-process-adapter-design-20260923.md b/harness/multiselect-process-adapter-design-20260923.md new file mode 100644 index 00000000..a46bec60 --- /dev/null +++ b/harness/multiselect-process-adapter-design-20260923.md @@ -0,0 +1,60 @@ +# Process group adapter candidate + +Read-only Astra investigation against the State follow-up working tree above `7e348d967d1787c052a145b7448636613a6a7054`. This is a proposed next adapter, not implementation, accepted semantics, or verification. Live ownership and priorities remain in Gitea #29. No source, build or GUI change accompanied the investigation. + +## Representation facts + +`diagram_usage.rs` already defines `Surface::Process(owner)`, exact `ProcessOccurrence(OccurrenceEndpoint { references, occurrence })`, local `ProcessLink`, and qualified `ProcessPreviewLink` targets. Local cards are semantic occurrences in owner steps; they reference reusable definitions and are not saved diagram placements. Different reference paths to the same defining occurrence are different selected targets. Legacy derived `OrderedPath` arrows lack relation identities: do not fabricate editable IDs or substitute endpoints. + +`topology/usages.rs::Graph::usage_bounds` provides exact displayed-target visibility, including bounded qualified previews. Hidden preview tiles do not fall back to their outer reference. Its approximate reveal rectangles are not adequate edge hit tests. Qualified preview content stays read-only until its definition is explicitly opened. + +## Movement candidates + +| Candidate | Assessment | +| --- | --- | +| Persist arbitrary XY | Conflicts with derived process topology and needs new schema; reject for this adapter. | +| Sequential singleton reorder | Produces click-order-dependent layouts and does not define a simultaneous operation; reject. | +| Stable block within one original column | Candidate: preserves existing topology and uses one existing `SetSteps` command. | +| Rewire links during a cross-column drag | Alters process execution meaning; requires a separate capability and explicit interaction. | + +Existing `topology/direct.rs::reorder` verifies local contributions and equal original graph X-columns, then validates `SetSteps`. `Graph::expanded_qualified` derives ranks from sequence links and legacy order, with steps supplying vertical order; cyclic fallback can depend on enumeration order. + +The proposed pure planner admits the whole nonempty set only if every target is a visible local occurrence with an empty reference path and belongs to this owner. Selected IDs and the destination must share one original column. Build the block in original steps order, then use the column-slot permutation below: preserve other-column indices and unselected order within this column. Object-anchored drops on selected items are no-op; a raw gap is a separate API. Check the complete rank map remains unchanged, especially cycle fallback. Distinguish no-op from a command so it creates no source/history entry. Preview and commit must consume this same planned order; do not freely translate cards. + +Before-card only could reuse existing behavior but makes moving to the end awkward. Before/after slots are the preferred candidate, subject to independent review and actual interaction testing. Participation of each occurrence-body type also needs a precise decision. + +## Hide and removal + +Process has no saved placements and therefore no existing Hide operation. Explain that limitation; do not silently substitute semantic deletion or add persisted hidden cards without a separate design decision. + +Homogeneous local-occurrence removal should present one aggregate impact review and apply one `Batch(Delete(...))`. Core deferred deletion already supports union cleanup. Preserve referenced work, exchanges, reused process definitions and opposite-layer counterparts. Initially refuse qualified or mixed sets in full; local-link-only deletion can be a separate extension. Never invoke singleton deletion once per member. Capture document, revision, surface and selection generation through confirmation, revalidate the whole set, and preserve pending property/source/forms. + +## Implementation boundaries and falsifiers + +Extend `app/multiselect.rs` with Process dispatch and exact primary paths, avoiding the assumption that every context is a saved diagram. Use `app/process_canvas.rs` for captured adapter commands. The input and reorder planner belong in `behavior/topology/direct.rs`; graph/render changes in `topology.rs`; exact visibility in `topology/usages.rs`; stable controls in `topology/view.rs`. Process currently lacks the needed canvas right-click branch, so contextual Add/Remove needs explicit transport into `model_menu.rs`. Aggregate confirmation belongs at `app/diagram_removal.rs` and its view. Reject stale and legacy singleton commands before App's shared side effects. + +The current shared group gesture requires saved placements and an XY anchor; reuse its lifecycle concepts, not that constructor unchanged. Compare a small shared threshold/cancellation component with a Process order-specific gesture before introducing abstraction. + +First useful outcome: exact local/qualified/link selection with whole-set refusals, plus one atomic same-column local-occurrence reorder and one Undo/Redo. Aggregate removal remains separate acceptance if omitted from that outcome. + +Tests must independently assert source-order blocks from noncontiguous reverse-click selections; unchanged other-column indices and unselected order within the affected column, relations and reusable definitions; identical referenced work represented by distinct occurrences; distinct qualified paths and collapse visibility; full refusal of cross-column/mixed/stale sets; no-op history; ordered pointer/focus/Shift/cancellation/camera behavior; no primary-only reconnect bypass; OA and SA, legacy exchange order, explicit branches and cyclic fallback. Native slot feedback, stable canvas/camera, source save/reopen and one-step history are required beyond compilation. + +## Independent critique: revise before implementation + +A fresh read-only Astra review found a concrete defect in global block splicing. Given steps `[a,u,b,v,c]`, one displayed column `[a,b,c]`, and selected `{b,c}`, dropping after `a` is visually a no-op. The proposed global splice produces `[a,b,c,u,v]`, changing source/history without a visible change. Equivalent before/after positions in the same displayed gap must have one interpretation. + +The revised candidate is a **column-slot permutation**: extract this column's order from original steps, remove the selected members, normalize the drop to a gap in the remainder, insert the original-order selected block, then write the resulting column order back into this column's original steps indices. Other-column indices remain exact; unselected relative order within the affected column is preserved. This deliberately replaces the earlier all-unselected-global-order condition. Return no command when column order is unchanged, and verify the complete ID-to-rank map remains unchanged before accepting `SetSteps`. Local Action, Exchange, Control and SequenceReference occurrences are proposed participants; qualified descendants and links are whole-set refusals. This revision still requires an independent pure experiment before UI implementation. + +Scope correction: legacy ordered-path nodes have successive ranks, and cycle-blocked nodes receive distinct fallback ranks (`topology.rs` rank computation). A multiple selection of those nodes cannot satisfy same-column admission. Test refusal and unchanged execution meaning rather than claiming useful reordering of those sets. A graph containing a cycle can still admit a separate acyclic peer group. + +Removal has an additional semantic consequence: `behavior.rs` derives explicit-versus-legacy interpretation from remaining occurrence kinds and SequenceLinks. Deleting action/control from `[action,exchange1,control,exchange2]` without links leaves an exchange-only sequence with implicit ordered-linear meaning. Aggregate review must expose this transition or initially refuse it. Include caller-owned ContextualLinks targeting removed occurrences in the impact union (`references.rs` cleanup), not just local links. See the presentation-versus-legacy distinction in `docs/language.rst`. + +Smallest production experiment: independent OA/SA fixtures with interleaved steps and different card heights; reverse-click selection; before-first/after-last and equivalent gaps; already-positioned no-ops; literal expected steps; complete Project equality outside steps; unchanged ranks; exact source roundtrip and one Undo/Redo. Add legacy and cycle refusal plus acyclic peers beside a cycle. Separately demonstrate deletion-induced mode switching before designing confirmation. These Rust/application checks have not run yet. + +## Bounded permutation experiment + +[Executable exploratory model and results](experiments/process-column-slots-20260923/README.md) isolate the order transformation from production architecture. An independent Astra investigator implemented it; the parent read both scripts and reran them successfully. The experiment covers 214,990 column cases and 861,096 plans against a four-node graph rank model, with no invariant failures. Its modeled ranks and sentinel serialization do not execute the Rust layout, source writer or history. + +An object-anchored Before/After interface can keep selected-target no-op without losing changed destinations: all remainder gaps were reachable using unselected anchors in 2,036 bounded subsets. Raw-gap normalization is different: in `[a,b,c,d]`, selecting `{b,d}` and normalizing Before `d` gives `[a,c,b,d]`; selected-target no-op keeps the original, while After unselected `c` reaches the changed order. This supports the simpler selected-target no-op interaction. Block contiguity applies to an admitted planned move, not to that no-op. Actual pointer feedback, production graph admission, serialization and history remain unverified. + +An independent critic matched the experiment's rank algorithm to Rust and found no counterexample under the stated valid-input assumptions. The review prompted an explicit five-node changed group beside a cycle; four-node enumeration could only cover singleton movement or unchanged whole-peer blocks there. It also identified a compatibility choice: current singleton `direct::reorder` globally splices, so moving `b` before `a` in `[a,u,b,v,c]` writes `[b,a,u,v,c]`, while this candidate writes `[b,u,a,v,c]` with the same display. Prefer one column-slot planner for both singleton and group reorder, preserving unrelated column indices consistently. This would intentionally revise singleton source ordering and requires production regression/roundtrip/history checks; no such migration is implemented here. Retaining global singleton splice was rejected as two source transformation rules for the same visible operation. Native anchor reachability and selected-target guard execution remain separate checks. diff --git a/harness/multiselect-saved-view-20260923.md b/harness/multiselect-saved-view-20260923.md new file mode 100644 index 00000000..15240791 --- /dev/null +++ b/harness/multiselect-saved-view-20260923.md @@ -0,0 +1,218 @@ +# Ordinary saved-view multiple selection — verification report + +**Verified branch contribution, not full issue acceptance.** This report covers the ordinary saved-view +subset of [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29). +Final source is `f6a6010baed9e6164a1fcc7ecd58256a562827d7` on +`codex/multiselect-9e8r6t-20260923`, based on the published State/attachment +checkpoint `b8685416f56e53e30bd11ecf93a54cea10ac8458`. The final Windows desktop +suite, strict lint, formatting, build and corrected-layout native run passed. +Integration with the release owner's later union remains separate work. + +## Evaluated source and scope + +Native run-1 used commit `12a7352ddef5f69a144ead8219a50916233d63a9` and the +729-file input map +`55a5f354bb5049a8c77ba9bafa833438ebf754649b0cf5d175d0c8dcfcedd7bb`. +Core-3, desktop-4, lint-2, format-1 and both native/checker builds share that +exact input map. Earlier test receipts record HEAD +`b8685416f56e53e30bd11ecf93a54cea10ac8458` because the feature was still +uncommitted; the captured source map, rather than HEAD alone, identifies their +tested contents. + +The corrected layout verification uses a distinct 729-file input map, +`99dc46645236e674244e7c4867595a6000d3d85b54bcc9ab2b0c601923ecf66c`, +captured by desktop-5 at HEAD `12a7352ddef5f69a144ead8219a50916233d63a9`. +An exact Git-blob comparison subsequently confirmed the complete captured input +inventory equals commit `f6a6010baed9e6164a1fcc7ecd58256a562827d7`. +Results for the earlier map do not establish the changed layout's behavior. +Desktop-5, lint-3, format-2, native-build-2 and windows-build-2 all use the +corrected map. Native run-2 operated that Windows build; its 117,519,360-byte +executable has SHA-256 +`49396ee5c24f749cbee54e1fd77e15f5d73b0e19cf246724216ce7464d3deba5`. + +The implemented subset provides exact transient saved-view selection with a +primary Inspector target, Shift toggling and contextual Add/Remove, deferred +stationary-click versus group-drag behavior, simultaneous placement movement, +and group Hide that preserves model objects. The complete typed selection is +admitted or refused; ports/edges are not silently filtered from a group action. +Captured operations reject stale document, revision, surface and selection +generation. Ordinary property edits remain primary-only. Movement uses one +atomic transaction and the same pure plan for preview and commit. + +Required behavior and remaining adapter families are defined by the +[contract](multiselect-contract-20260923.md). Design and detailed review evidence +remain in the [core review](multiselect-core-review-20260923.md), +[desktop review](multiselect-desktop-review-20260923.md), +[fresh review](multiselect-fresh-review-20260923.md), and +[native evaluator/layout review](multiselect-native-review-20260923.md). + +## Closed gates + +The closed receipts below report unchanged captured inputs and successful exit. +Windows used `stable-x86_64-pc-windows-gnullvm`, Cargo 1.98.1 and rustc 1.98.1, +locked/offline dependencies, and the supported headless desktop renderer. + +| Gate | Command/result | +| --- | --- | +| core-3 | `cargo test --locked --offline -p oa-core --test group_movement --test state_diagrams --example multiselect_native -- --nocapture`: 7 movement + 13 existing State + 10 evaluator tests passed; zero failures. State test passage does not accept a State group UI adapter. | +| desktop-4 | `cargo test --locked --offline -p oa-desktop -- --nocapture`: 1,256 passed, zero failed, six ignored. Includes mounted focus, passive modifiers across I/O, selection/history and pointer-lifecycle regressions. | +| desktop-5 | Same desktop command, corrected layout: 1,257 passed, zero failed, six existing ignored. The added mounted regression preserves canvas bounds, fitted camera, complete Project, source and empty history across empty/single/group/mixed selection. | +| lint-2 | `cargo clippy --locked --offline -p oa-core -p oa-desktop --all-targets -- -D warnings`: passed. | +| lint-3 / format-2 | Strict all-target Clippy for core/desktop and `cargo fmt --all -- --check` passed on the corrected layout input map. | +| format-1 | `cargo fmt --all -- --check`: passed. | +| native-build-1 / windows-build-1 | Locked/offline checker example and Windows desktop executable builds passed, with matching input maps and recorded executable hashes. | +| native-build-2 / windows-build-2 | Both builds passed from final source `f6a6010`, with matching corrected-layout inputs and executable pins. | + +Failures were retained and corrected, not counted as passing evidence: +desktop-1 exposed module visibility/menu-token compile errors; desktop-2 +reported one Placement-properties selection regression (1,247 passed, one +failed, six ignored), corrected by recognizing the selected placement identity; +core-2 exposed the evaluator incorrectly rejecting ordinary fixture warnings; +oracle-2 then exposed an unqualified Severity type; lint-1 rejected an +unnecessary clone. Later closed gates above cover the corrected source. +Desktop-3's earlier 1,254/0/6 pass preceded the two passive-modifier suspension +regressions included in desktop-4. Full historical commands, inputs and logs +belong in the checks package, not this summary. + +## Native behavior and saved-state evidence + +The owning session operated each pinned Windows app through contextual Add for +A/B, one group drag, Save, movement Undo/Redo with Save, return to baseline, +group Hide, Hide Undo/Redo with Save, and reopening the hidden saved source. +These are actual Computer Use observations, not inputs independently replayed +by the evidence verifier. Run-1 retains 27 numbered native captures and run-2 +retains 24. Each run has nine saved-source snapshots with evaluator receipts, +process/start/finish records and `history-bytes.json`. + +Run-1 records PID 7540 / window 131836; run-2 records PID 5312 / window 20055002. +Each capture names the corresponding pinned executable path and consistent +window identity. Both closing receipts report exit zero, unchanged GUI/checker +binaries and fixture, and empty stdout/stderr. These are joined historical +records, not independent OS authentication of every captured input. + +The independent checker compares the complete Project against explicit fixture +expectations and validates JSON/source roundtrips. All nine final snapshots +in each run (18 total) report PASS with stable source, baseline and checker pins: + +| Saved state | Verified relationship | +| --- | --- | +| `baseline`, `move-undo`, `before-hide`, `hide-undo` | Equal to the complete baseline Project; all nine source files byte-identical to the fixture baseline. | +| `move`, `move-redo` | A, B and A's unselected child share delta `(100, 60)`; internal route pins translate once. C, one-ended pins, attachments, labels, semantic records, traces and other views remain exact. The two source inventories are byte-identical. | +| `hidden`, `hide-redo`, `reopened-hidden` | Only A/B placements are removed. The child placement, stored routes, model objects and other views remain exact. These three source inventories are byte-identical. | + +Across both changed states, the only changed source file is +`views/diagram_oa_view.arcadia`; the other eight source files remain unchanged. +This supports precise saved-state restoration across the observed +one-step history sequence. Equality alone does not authenticate Undo button +presses or establish history behavior without those observations. + +The fixture is deliberately incomplete engineering content. The parent observed +11 warnings: `missing_system` ×1, `unallocated_leaf` ×4, +`work_without_behavior_context` ×4, `exchange_without_behavior_context` ×1, +and `non_leaf_work_exchange` ×1. The native status shows 11 model checks; these +are not presented as new application defects or zero-warning acceptance. +The evaluator permits completeness warnings but rejects source errors and +compilation failures. + +Run-1 also exposed a real layout issue: forming a group inserted a conditional +toolbar block, resized the canvas and shifted nodes without a geometry edit. +Independent inspection of captures 06 and 08 confirms the shift. The subsequent +correction reserves a fixed selection/status strip, adds a visible +Shift-click hint and includes a mounted bounds/camera regression. Run-1's pinned +binary is unchanged and cannot verify that correction. Desktop-5 passed, and +run-2 then confirmed the corrected behavior: captures 03/05/07 retain the same +canvas top (about y=341), 84% zoom, and node positions across empty/single/group +selection. The count, Hide/Clear controls, primary-only disclosure and Shift +hint are readable at the observed 1442×932 window size. + +Run-2's drag was from `(405,533)` to `(489,583)` in window coordinates, an +84-pixel right / 50-pixel down movement at the displayed 84% zoom. The observed +geometry moves accordingly; the independent saved-model oracle finds the +grid-aligned common world delta `(100,60)`. One Ctrl+Z restores baseline, and one +Ctrl+Y restores the moved state. Save and complete-Project checks follow each. +Hide at `(377,287)` removes A/B placements while leaving the child and C at their +previous screen positions; Undo restores A/B without the former clipping or +viewport shift. Zoom remains 84% throughout those edits. Reopening the hidden +source performs the normal fresh fit (88%); camera is transient, and the saved +Project/source remain exact. Capture labels identify each stage. + +The initial run-2 baseline checker invocation omitted the shell's GNU toolchain +runtime setup and exited `3221225781` with empty streams. The same checker and +byte-identical source passed after environment setup. The failed snapshot, +retry receipts and exact helpers are retained in the separate +[incident record](experiments/multiselect-20260923/incidents/README.md); it is not +counted as a successful checker attempt or an application crash. + +## Reproduce and inspect + +Use the repository's [Windows build/packaging guidance](../docs/windows-packaging.md) +and a configured Rust/GNU runtime environment. Captured runs used Rust/Cargo +1.98.1, LLVM-MinGW 20260826, one Cargo job and `CARGO_INCREMENTAL=0`. +`--offline` requires the dependency cache to be present. From the source checkout: + +```text +cargo test --locked --offline -p oa-core --test group_movement --test state_diagrams --example multiselect_native -- --nocapture +cargo test --locked --offline -p oa-desktop -- --nocapture +cargo clippy --locked --offline -p oa-core -p oa-desktop --all-targets -- -D warnings +cargo fmt --all -- --check +cargo build --locked --offline -p oa-desktop --bin arcadia-studio +cargo build --locked --offline -p oa-core --example multiselect_native +``` + +Run the resulting desktop executable with the GNU runtime directory on PATH. +The built `multiselect_native` example accepts `generate NEW_DIRECTORY`; keep +the generated fixture unchanged, copy its `source` directory to a fresh working +directory, and open that copy's `project.arcadia` in the app. After saving each +observed operation, evaluate with: + +```text +multiselect_native check unchanged FIXTURE/baseline.json WORKING/project.arcadia +multiselect_native check moved FIXTURE/baseline.json WORKING/project.arcadia +multiselect_native check hide FIXTURE/baseline.json WORKING/project.arcadia +``` + +Use the mode matching the table above. Preserve separate saved source copies +for the nine named stages and compare their exact inventories/bytes in the +three stated equality groups. Commands cannot substitute for the actual native +selection, movement, history and reopen actions. + +Portable verification from a repository containing the named source commits: + +```text +python -B harness/experiments/multiselect-20260923/checks/verify.py +python -B harness/experiments/multiselect-20260923/native/verify.py +python -B harness/experiments/multiselect-20260923/source/verify.py --repo . --checks harness/experiments/multiselect-20260923/checks +``` + +The packages retain original logs, inputs, captures, fixtures and snapshot +receipts. The source verifier freshly compares all captured Git blobs and the +complete filtered inventory at both named commits. These offline checks do not +execute archived code or rerun the GUI; consult their READMEs for precise limits. + +## Acceptance limits and delivery + +- Both native runs exercise OA Activities in an ordinary Overview, not a native SA + Function workflow. Native selection used contextual Add because the chosen + tool cannot inject held-Shift plus click; mounted tests provide separate + Shift-before-focus evidence. The App-level multiselect tests separately use + SA Functions, a nested Function and a FunctionalExchange in a Functional + Dataflow view; that automated coverage is not a native SA interaction claim. +- The checker infers a bounded common delta from saved A. It verifies group + invariants, not the exact intended pointer destination or drag magnitude. + Native action observations, model equality, byte equality and mounted tests + are distinct evidence and are not interchangeable. +- State, Allocation, derived Process/Scenario group adapters and aggregate + model deletion remain incomplete. This report does not close the full issue, + accept those families, claim Linux verification, or represent an integrated + release. The native runs do not cover every mixed-target, cancellation or + suspension scenario exercised by automated tests. +- Native layout review covers 1442×932, not every minimum-window, DPI or keyboard + accessibility condition. No broad usability acceptance is inferred from this + bounded fixture. +- Portable evidence is supplied under + [checks](experiments/multiselect-20260923/checks/README.md) and + [native](experiments/multiselect-20260923/native/README.md), with separate + [source identity](experiments/multiselect-20260923/source/README.md) and incident + records. The checks package retains 19 closed runs, including five nonzero + historical gates. Raw absolute paths in the receipts are historical labels; + the versioned packages make the evidence consumable on other machines. diff --git a/harness/multiselect-state-20260923.md b/harness/multiselect-state-20260923.md new file mode 100644 index 00000000..b8acbd9b --- /dev/null +++ b/harness/multiselect-state-20260923.md @@ -0,0 +1,106 @@ +# State multiple selection — branch evidence + +This contribution continues [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29) above the delivered ordinary saved-view head `7e348d967d1787c052a145b7448636613a6a7054`. It is not a combined release or completion of the full multiple-selection requirement. Integration remains with the owner in [issue 27](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/27). + +Source commits on `codex/multiselect-state-9e8r6t-20260923`: + +- `2a3b9ba822a49ead233c4b5f98412a07c1c1da12`: State adapter and independent evaluator, exact source of the full desktop result. +- `3480b54201a5f361cca0bcd0d45dd6546a7a7bc9`: box the optional exact menu target to keep the menu event small. Three files change; matching/selection behavior is unchanged. + +The scoped Windows gates and native workflow below pass. Portable evidence is included with this contribution; combined integration and release acceptance remain separate. + +## Behavior and implementation + +State views share the transient typed selection model with ordinary saved views. Shift-click and contextual Add/Remove preserve exact placement, transition-edge and action/internal-transition row identities. A row includes its owner placement and relation, so replacing a placement or reassigning a relation cannot silently bind an old selection to another occurrence. Primary inspection uses State's own placement field; row inspection does not promote the owner placement or reusable work target. Properties edit only the primary item. + +Placement groups move through one simultaneous core plan and one document command. Preview applies only the returned node/route changes to a cloned diagram, not a full Project clone or a sequence of singleton moves. State movement retains fractional unsnapped displacement; ordinary saved views retain their ten-unit grid and State bend editing its five-unit grid. Composite descendants and boundary-projected entry/exit symbols use their authored anchors and move once; hard pins translate only when all required endpoint/boundary owners move. Authored attachment/label metadata stays unchanged even when derived paths and label positions rerender. + +Pressing a selected member defers primary changes until the gesture outcome. A stationary release collapses to that item; movement beyond threshold remains a group gesture even after returning to the start. Escape, focus loss, changed viewport/tool/selection/document context, or suspended editing retires captured gestures. Mixed row/edge/placement groups refuse movement and Hide as a whole. Group menus expose captured-set actions, without singleton destructive fallbacks. Placement-only Hide removes just selected placements in one history entry; model elements, relations, reusable work and other views survive. + +State reserves fixed selection-feedback and unplaced-picker space so selecting, hiding and Undo do not resize/refit the canvas. Mounted checks exercise those transitions and focus ownership. Singleton transition routing remains available with the new selection path: bend snapping/commit/cancel, endpoint stationary/cancel/commit and Shift priority have dedicated production-input checks. + +Stale envelopes and queued legacy singleton State Select/Move/Remove events under a group are rejected before shared App navigation/reveal/allocation side effects. Dirty forms/source, active tools, read-only context and busy I/O preserve their established edit ownership. + +No schema, persistence field, public REST command or new core geometry command is introduced. Important seams for integration are `app.rs` early admission/reconciliation, `app/multiselect.rs`, State context menu target transport, State canvas input/preview, and fixed State controls. Preserve the integration branch's newer Scenario/source/Properties logic. The independent example may need the newer `Diagram.layout: None` field when composed with the integration owner's schema extension. + +## Review and evaluators + +[State design](multiselect-state-adapter-design-20260923.md), [fresh independent review](state-multiselect-review-20260923.md), and [native evaluator design](state-multiselect-native-design-20260923.md) record the contract and falsifiers. The independent critic did not author the adapter or evaluator. Two verified findings were fixed: fractional-delta inference from a nonzero checker anchor, and late queued-singleton refusal mutating shared App state. The reviewer rechecked both corrections statically; parent tests supply execution evidence. + +The deterministic native example constructs a composite A, unrelated B, fixed C, nested region/leaf, projected entry/exit points, internal and external hard routes, shared work, another State view, and SA/trace records. Its checker independently enumerates the expected complete Project for unchanged/moved/Hide modes. A is at the authored origin so its saved position preserves the actual `f32` group delta, including nonbinary fractions. This verifies resulting model invariants, not exact pointer-to-world fidelity. It rejects malformed source and unrelated changes; eleven tests include genuine command results and corruption falsifiers. + +## Completed Windows checks + +Environment: Rust/Cargo 1.98.1, `stable-x86_64-pc-windows-gnullvm`, LLVM MinGW 20260826, one Cargo job, incremental compilation disabled. Linux/WSL checks remain user-paused. + +| Capture | Source | Result | +| --- | --- | --- | +| `core-2` | `2a3b9ba` | 11 State native-evaluator tests passed | +| `desktop-2` | `2a3b9ba` | 1,286 passed, 0 failed, 6 existing ignored; includes 29 added cases | +| `lint-2` | `3480b54` | strict core/desktop Clippy, all targets, passed | +| `format-1` | `3480b54` | workspace formatting check passed | +| `desktop-3` | `3480b54` | 122 focused State/menu tests passed; 1,170 filtered out | +| `native-build-1` | `3480b54` | matching State evaluator build passed | +| `windows-build-1` | `3480b54` | matching Windows GUI build passed | + +Full desktop compile took 13m20s; execution 607.72s. Core-2 and desktop-2 share 733 unchanged inputs (map SHA-256 `9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a`). Lint-2 and format-1 share 735 unchanged inputs (map `4215d0ca0319282768f1acdf05c2c094ea7432e17ed3a715df73aa05cef851bf`). Parent compared complete inventories and exact Git blobs at both source commits. Recorded run HEAD alone does not identify the originally uncommitted bytes: the first source commit preserves the earlier three menu-field blobs from the verified desktop capsule, followed by the boxing correction, without replacing the working files. + +The revised 735-input scope adds two existing literal compile-time fixtures outside the usual source directories: `harness/experiments/edge-attachments-20260922/native-fixture.arcadia` and `harness/experiments/textual-authoring-20260911/alarm-response.arcadia`. An audit covered 71 Rust literal include references with no unsupported pattern. The historical 733-input desktop/core-example captures contain their actual compile-time dependencies; historical all-target lint lacks the core attachment fixture and does not establish full all-target source identity. + +Commands (with the described toolchain environment): + +```text +cargo test -p oa-core --example state_multiselect_native --message-format=json -- --test-threads=1 +cargo test -p oa-desktop --message-format=json -- --test-threads=1 +cargo test -p oa-desktop --message-format=json -- state_ model_menu --test-threads=1 +cargo clippy -p oa-core -p oa-desktop --all-targets --message-format=json -- -D warnings +cargo fmt --all -- --check +``` + +Final focused compilation took 9m26s and test execution 30.85s. The 122 tests cover the changed State/menu paths; the complete earlier suite remains tied to `2a3b9ba` rather than relabeled as final-source execution. Both native binaries use the identical final 735-input map. + +## Native Windows observation + +The exact recorded GUI and checker ran against an immutable generated fixture and a separate working source copy. The GUI image is 117,615,104 bytes, SHA-256 `01cc30dac3a71e847415c86e2e08b0da45989aac260a82bbb00f048c3eb8a448`. Run 1 closed normally with exit 0; stdout/stderr are empty, and both binaries and the fixture remained unchanged. The archive retains 35 app-window observations (main window 1442×932, plus related transient images), ten successful complete-Project snapshots, and their helpers and build receipts. + +Observed sequence: + +1. Open and save the State source baseline. Select composite A, context-add its exact entry-action row, and drag the selected State. The mixed set is refused; row-primary Properties still describe the StateAction and reusable work remains separate. Saving gives the unchanged complete Project. A stationary State click collapses to A without geometry changes. +2. Context-add B. The two placement outlines and primary-only Properties disclosure appear without a viewport jump. Drag nonprimary A by 47×29 screen pixels at the displayed 79% zoom. Both roots, A's descendants and eligible internal pins move; B stays primary and C stays fixed. The saved checker infers exact shared displacement `(59.72580337524414, 36.85209274291992)` and matches the independently expected complete Project. +3. Save, one Undo, save, one Redo, save. All three checks pass; Undo restores baseline bytes and Redo restores the moved bytes across all nine source files. +4. Undo back to baseline, save, open B's selected-member menu. It reports two selected, offers group Hide and disables aggregate model deletion. Hide removes A/B placements, clears their transient membership, retains model-tree elements and the inner region/leaf, and leaves C's screen position unchanged. The unplaced picker lists A and B. One Undo/Redo with saves restores the exact baseline/hidden source byte groups. +5. Reopen the saved project through the native file dialog, then save again. A/B remain hidden, the selection is empty, and the complete Project and nine source files exactly equal the earlier hidden state. Reopening fits the surviving content (71%); camera state is transient and this is not a claim of persisted viewport equality. + +Only `views/diagram_state_view.arcadia` changes for either movement or Hide. The nine required history snapshots form three distinct exact-byte groups; the additional `row-mixed-unchanged` snapshot also equals baseline. The checker conserves all other model/source records, including the other State/SA views, shared work, trace/refinement relations, route attachments and labels. Hide deliberately removes only selected placements: the extreme authored entry/exit anchors become exposed when their displayed State boundary is absent; they extend beyond the current viewport until Fit/reopen. No authored anchor changes are introduced to conceal that derived geometry. + +Native limitations: contextual Add exercises actual user operations but does not inject held Shift; mounted/input tests cover modifier lifetimes. No minimum-window/DPI, SA native trial, aggregate semantic deletion or combined-release claim is made. The fixture has 13 model-check findings visible in the UI; this finite geometry/identity evaluator requires valid source and exact expected Project, not a completeness-warning-free engineering model. + +A fresh independent native review inspected representative actual screenshots, all ten checker receipts, the source snapshots and outer package pins. It found no new blocker for this bounded State delivery and confirmed the scope distinctions above; it did not rerun archive verifiers or Git comparison. The review also noted visible overlap between the fixture's multiline A→B label, its secondary summary and nearby row area before and after movement. This report does not claim flawless State label layout. The picker was inspected for recovery choices, but selecting a choice to restore a placement was not exercised; recovery through Undo was exercised. + +The native trial exposed [issue 35: picker Escape dismissal](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/35). Capture 24 opens the picker, 25 shows Escape leaving it open, and 26 shows successful outside-click dismissal. A fresh code review identifies existing shared Iced picker behavior, not a State multiselect regression; the creation/source-modal Escape priority also needs verification before a shared fix. This recoverable usability defect remains open, not silently treated as accepted behavior. + +Automation recovery is retained: the first reopen attempt navigated the file dialog into `views` instead of opening the project; one dialog capture returned `no monitor found`, then fresh window selection/activation recovered it. Captures 32/33 have intent-based filenames but still show the open dialog. Successful explicit-path reopen is capture 34; save is 35. No failed attempt is counted as a successful reopen. + +## Portable evidence + +- [Checks](experiments/state-multiselect-20260923/checks/README.md): nine closed captures, including two failed gates, with three exact source-capsule pairs and both historical helpers. +- [Native](experiments/state-multiselect-20260923/native/README.md): run 1 screenshots, ten complete-Project checks, three-group source history, fixture and executable provenance. +- [Source identity](experiments/state-multiselect-20260923/source/README.md): exact 733/735-input relationships to the two source commits; optional renewed Git-blob comparison. +- [Invalid first-capture incident](experiments/state-multiselect-20260923/incidents/core-1/README.md): original bytes and the single known mismatch, never an accepted gate. + +Run with Python 3.10+ from the repository root: + +```text +python -B harness/experiments/state-multiselect-20260923/checks/verify.py +python -B harness/experiments/state-multiselect-20260923/native/verify.py +python -B harness/experiments/state-multiselect-20260923/source/verify.py --repo . +python -B harness/experiments/state-multiselect-20260923/incidents/core-1/verify.py +``` + +These commands verify archived bytes/relationships and optional source identity, not new execution or authentication. To reproduce the native trial, build both packages at `3480b54`, run `state_multiselect_native generate NEW_FIXTURE_DIRECTORY`, copy its `source` directory, open that copy's `project.arcadia` in the GUI, and use `state_multiselect_native check unchanged|moved|hide BASELINE_JSON SAVED_PROJECT_ARCADIA` at the corresponding stages. The checker requires the MinGW runtime environment on Windows. + +## Historical failures and scope + +Keep the first desktop compile failure (a test helper shadowed by a local placement UUID) and first strict lint failure (large enum variant) with their actual exit 101. Neither is a pass. Core-1 recorded eleven passing tests but is an invalid capture: concurrent formatting changed `model_menu.rs`, yielding a false unchanged-input flag and one map/archive mismatch. Its exact seven raw files and helper belong in a separately pinned incident, never the strict accepted-capture package. Stable core-2 supplies the actual accepted result. No historical receipt is repaired or relabeled. + +Process, Allocation and Scenario group adapters and aggregate semantic deletion are still required for issue 29. Unsupported groups currently receive whole-set refusals. The [Process candidate and bounded permutation experiment](multiselect-process-adapter-design-20260923.md) are exploratory evidence only, not implemented functionality. Full combined release, minimum-window/DPI behavior and Linux acceptance are not claimed here. diff --git a/harness/multiselect-state-adapter-design-20260923.md b/harness/multiselect-state-adapter-design-20260923.md new file mode 100644 index 00000000..f5ef831f --- /dev/null +++ b/harness/multiselect-state-adapter-design-20260923.md @@ -0,0 +1,108 @@ +# Persisted State multiple-selection adapter + +Scope: next required adapter for [issue 29](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/29), following the [selection contract](multiselect-contract-20260923.md). This is a code-grounded proposal, not implementation or acceptance evidence. Inspected local branch `codex/multiselect-9e8r6t-20260923`, base `b8685416f56e53e30bd11ecf93a54cea10ac8458`, including uncommitted ordinary-selection work on 2026-09-23. Parent owns integration; no build or GUI checks were run for this investigation. + +## Verified interfaces and present workflow + +- `diagram_usage::{Surface, Target}` already represents this surface as `Surface::Saved(diagram)` and a state node as `Target::Placement(placement)`. There is **no** `StatePlacement` variant and none is needed. Action and internal-transition rows have `Target::StateRow { placement, relation }`; rendered transition edges have `Target::Edge(relation)`. +- `behavior/state_view.rs::view` constructs `StateCanvas` using `state_context()`, semantic `app.selected`, its own camera/tool, and existing draft guards. It wraps the mapped canvas in the same production `diagram::focus::wrap` as Architecture, including `app.busy` suspension. Its optional unplaced-elements picker deliberately occupies a stable slot to preserve the widget focus tree. +- State primary placement lives in `app.behavior.diagram.placement`, not `app.selected_node`. `app/state_diagram.rs::edit_state_diagram` selects semantic identities and clears `selected_node`. `App::refresh_draft` rediscovers the State placement from the semantic identity; therefore exact-target reconciliation must override that discovery when an occurrence disappears or is replaced. +- `state_canvas::{hit, node_at}` respects symbol hit shapes, labels, nesting and row priority. `Hit::Node` preserves placement and element; `Hit::StateAction(Id)` and internal rows represented by `Hit::Transition(Id)` currently discard the row's placement identity. Do not infer a group target from semantic kind alone. +- `StateCanvas::update` uses ordered window coordinates, a 3-pixel movement threshold, and stored press geometry. It currently selects on a node press and emits one `state_edit::Event::Move` on release. It has distinct attachment, bend, transition, reconnect, creation and pan gestures. Escape resets these gestures and selects the Select tool. Right-click resets provisional geometry before capturing the menu target. +- `state_canvas::scene_with_bounds` currently clones the diagram, applies a singleton movement plus descendants and `translated_routes`, then projects and routes the result. Attachment and bend previews are independently cached; bend preview reroutes only its edge against a baseline. `state_edit::plan(Event::Move)` commits through the existing singleton `Command::MoveNode`. +- `state_menu::popup` runs before the ordinary group-menu branch in `model_menu::popup`; merely broadening the ordinary App selection context will not provide State group menus. Current State menu offers singleton Hide, semantic deletion and route tools. `app/diagram_removal.rs` also distinguishes a placement Hide from a row/edge's semantic deletion confirmation. + +User workflows to preserve: choose a machine and saved view; inspect state headers, entry/do/exit rows and transitions; select tools to add symbols or reconnect transitions; move composite states with folded children; adjust hard route pins and attachments; Hide placements and recover them through the unplaced picker or Undo. Group selection must coexist with these workflows, not silently reinterpret rows as nodes or alter route tooling. + +## Recommended minimal coherent adapter + +Use the existing transient `Selection` and its snapshot generation without adding model/source schema. Share the App's Saved-view group operations, but make diagram resolution, primary inspection and visibility surface-aware. A State view remains a Saved surface; switching between pages or behavior owners explicitly clears the set even if both resolve to the same diagram UUID. + +Select-tool hit mapping: + +| Rendered hit | Exact target | Initial group movement/Hide capability | +| --- | --- | --- | +| State, machine, region or pseudostate placement | `Placement(node.id)` | Placement-only set admitted | +| Routed transition | `Edge(relation.id)` | Select/inspect; refuse group placement actions | +| Entry/do/exit action or internal-transition row | `StateRow { placement, relation }` | Select/inspect; refuse group placement actions | +| Navigation highlight / empty content | No edit target | Never promote to selection | + +Keep individual route editing available after choosing a singleton edge. When several targets are selected, selected-edge handles must not bypass the group operation contract: a mixed drag is refused as a whole, and a stationary selected-member click can collapse to that edge before its existing tools are used. Shift-click takes precedence over singleton bend insertion/reconnect hit paths in Select mode. Other active tools keep their existing ownership; switching tools retires gesture snapshots through the canvas epoch. + +Use `diagram_edit::translate_placements` once for the full captured placement set. Apply its returned `Batch(EditNode, SetEdgeRoute)` through `Document::execute` once; after success restore the typed primary instead of adopting the Batch's final selected edge/node. Preview applies those same node/route commands to a cloned diagram and projects with the unchanged project semantics. Do not call `prepare_command` on a cloned whole project for every pointer frame. The ordinary preview already follows this bounded approach and is the reference to factor if sharing becomes simpler than two tiny adapters. + +### Entry/exit geometry is a required distinction + +State projection constrains EntryPoint/ExitPoint display geometry to their owner's boundary, while authored placement positions remain stored. The movement planner must use authored positions and let `projection::project_diagram` derive displayed geometry. `projection::translated_routes` already calls `state::boundary_moves_with`: moving two endpoints does not translate pins if their entry/exit owner's boundary placement remains fixed. Moving the boundary and those descendants together translates pins once. A partial view without the boundary must retain the helper's existing rule; do not invent an implicit boundary placement. + +Do not promise that moving an entry symbol's authored X coordinate causes the same displayed X displacement when its boundary is fixed. Preserve current State projection behavior and verify stored positions, projected symbols and route pins separately. This adapter does not change symbol sizing, container resizing, or route/traceability policies. + +### Pointer helper reuse has one consequential limit + +`diagram/group_gesture.rs::Gesture` provides snapshot capture, viewport validity, irreversible threshold crossing, stationary collapse and return-to-origin no-op handling. Its current displacement is explicitly snapped to a 10-unit grid. Existing State node movement is unsnapped; State bend movement separately snaps to 5 units. Blindly reusing its constructor changes State movement behavior. + +Preferred reversible design: reuse the lifecycle helper with an explicit small displacement policy (`Unsnapped` for State, existing grid behavior for Architecture), or separate displacement calculation from that helper. Preserve bend snapping unchanged. Avoid implementing another subtly different threshold/cancellation state machine merely to preserve the State delta calculation. Shared helper tests must cover both policies, off-grid starting points and out-and-back drags. This is an implementation choice, not a request for user approval. + +## Concrete touch map + +| File / function | Required change | Reusable boundary | +| --- | --- | --- | +| `app/multiselect.rs::multiselect_context` | Accept `Page::Behavior` only when `state_context()` resolves a real State view; retain Architecture path | Same `Context`, `Scope`, Saved ID, snapshot generation | +| `app/multiselect.rs::multiselect_boundary` | Include resolved State view identity and State primary placement so owner/view changes trigger reconciliation | Existing revision/document/page invalidation | +| `app/multiselect.rs::{visible, legacy_target, inspect_multiselect_primary, reconcile_multiselect}` | Resolve diagram from live context, use State placement field, admit exact visible State rows; retain lost-primary survivor behavior against semantic remapping | Existing exact placement/edge handling and stale-generation checks | +| `app/multiselect.rs::multiselect_event` | Require State Select tool for group selection/move/hide, preserve pending behavior/property/source guards, clear State bend/attachment primary details appropriately | Existing `placements`, atomic movement and Hide transaction | +| `behavior/state_edit.rs::Event` and `state_view.rs::event` | Transport typed selection events from `Canvas` to App; use a small event wrapper rather than duplicating selection commands | Existing `Message::DiagramSelection` handler | +| `app.rs::update` early admission | If selection events remain wrapped in `Message::StateDiagram`, validate their envelope **before** allocation/camera/reveal boundaries; alternatively map them to `Message::DiagramSelection` at the widget boundary and still guard any directly constructible nested variant | Ordinary stale-operation guard | +| `app/state_diagram.rs::edit_state_diagram` | Route wrapper to group handler without calling generic `apply`; preserve singleton state mutation plans; reconcile after accepted create/delete/Undo | Existing `validate_context`, `execute`, state form guards | +| `app.rs` navigation / `reset_state_view_after_navigation` | Clear set on BehaviorDiagram, owner navigation and explicit page transitions; detect implicit fallback to another saved State view | Existing explicit Architecture selection reset | +| `behavior/state_canvas.rs::{State, Press, update}` | Passive modifier tracking/reset, exact hit targets, group press lifecycle, no primary changes during selected-member press, Escape cancel-before-clear | Focus wrapper and shared group lifecycle | +| `behavior/state_canvas.rs::{Hit, hit, visible_row_relation}` | Preserve row placement in hit result; add exact row visibility predicate that checks this placement and relation, not any row in the diagram | Existing `state_action_rows` / `internal_rows` geometry | +| `behavior/state_canvas.rs::{scene_with_bounds, CachedScene, PaintKey, draw}` | Group planned-diagram preview; membership/primary-aware paint keys and rings for nodes, edges and rows; retain separate bend cache | Existing State projection, boundary rules and routing | +| `behavior/state_view.rs::view` | Pass selection into StateCanvas; show count and group actions without unstable sibling insertion before canvas | Existing stable missing-controls slot, focus wrapper | +| `behavior/state_menu.rs::popup` | Add/Remove exact target before singleton actions; selected-member multi-menu returns explicit group actions and suppresses singleton destructive/route actions | Tokenized menu dispatch already preserves set for DiagramSelection events | +| `app/diagram_removal.rs` / State Delete dispatch | For multi-selection emit full-set Hide or explicit unsupported mixed/destructive reason; never fall through to primary-only deletion | Existing single-placement/row semantics remain for singleton | + +Context-menu rows need exact placement metadata as well as semantic relation ID. `model_menu::Request::placement` currently identifies the node to Hide, so do not overload it to mean a row owner without auditing singleton invocation. Prefer adding an optional typed selection target to the request (or a dedicated State capture field) while preserving existing semantic object/placement fields used by old actions. This is a boundary change justified by row occurrence identity, not a new serialization format. + +Placement-only Hide removes only explicitly selected placements, not the movement descendant union; hidden descendant placements and engineering objects survive, as in current ordinary tests. Group semantic deletion remains a separate aggregate-impact transaction/review requirement from the contract. Do not ship an enabled primary-only Delete action under a group label. A first coherent State adapter may explicitly refuse unsupported mixtures and group semantic deletion while the shared aggregate-removal implementation is developed; record that limitation, rather than treating it as full issue completion. + +## Independent falsifiers and acceptance tasks + +1. Two unrelated states with internal hard pins: independently enumerate expected node and pin positions. One simultaneous move translates pins; one-end route remains fixed. Assert the entire expected Project, exact source recompile, one Undo/Redo, other State views and opposite-layer records unchanged. +2. Select composite plus visible descendant, then fold the composite. Reconciliation prunes invisible selected targets while moving the still-selected composite carries hidden descendants exactly once. Test a directly captured nested set before folding as well. +3. Select EntryPoint and inner leaf with fixed boundary: pins stay in world coordinates. Select the boundary as well: pins translate once. Include a partial view omitting the boundary and an ExitPoint case. Verify authored versus projected position separately. +4. Shift held while Inspector owns focus, then first State click: exact target toggles. Passive modifiers during busy I/O do not acquire focus; window unfocus resets them. Typing/Delete/F2 while unfocused remain denied. Use the mounted App's production focus wrapper, not only Canvas Program tests. +5. Native event batches: press on a selected member, inspector would otherwise change canvas layout, then motion/release. No primary mutation before gesture outcome; no singleton command. Stationary release collapses; threshold-crossing out-and-back/no-op retains set and creates no history. Fit/pan/viewport change retires stale geometry without redefining resize policy. +6. Shift-click entry-action and internal-transition rows: target includes exact owner placement. Mixed row/node and edge/node movement or Hide refuses the whole set. A row cannot silently select/move its reusable work target or owning state. +7. Select transition alone and exercise existing bend, endpoint and attachment interactions. Repeat with the transition in a group: group admission takes precedence. Cancel a provisional bend, right-click and verify committed pin indices remain unchanged. Reuse all existing State bend/attachment gates. +8. Old Move/Hide/Collapse/Refuse snapshots after view switch, owner change, revision, remove/re-add ABA, tool change, same-semantic placement replacement: no model/source/history/selection/status/allocation side effects. Validate top-level App admission, not only state-edit planner rejection. +9. Open menu on an unselected row/node, dismiss: preserve set/primary. Add retains prior members. Selected-member menu hides the complete captured placement set and offers no singleton Delete. Menus from former views never apply after navigation. +10. Pending valid and invalid property forms, source drafts and State forms: preserve raw input, model and source; do not auto-apply before group edit. Hide/Undo restores exact authored source and geometry; primary-survivor rules do not resurrect removed selection membership. + +## Resolved questions and remaining bounded inspection + +Resolved from code: no new target type is needed for State placements; row hits need stronger occurrence identity; State has unsnapped node moves; boundary route behavior is already in core; menu dispatch and App selection boundary must be extended beyond a page test. + +Before implementation, inspect the actual then-current ordinary adapter and `group_gesture` API (they are still local work), and enumerate all `StateCanvas` struct literals in existing tests for the added selection field. Inspect `behavior::selected_owner`, BehaviorDiagram/owner navigation and any early-return App paths together when extending reconciliation; owner changes must not survive because `active_diagram` stayed constant. These are discoverable code questions, not user decisions. No additional external research or schema change is indicated. + +## Follow-up falsification review of the proposal + +Read-only follow-up against the current local implementation confirms the approach, with these tighter constraints. This is a second pass by the same investigator, not an independent-agent approval or execution evidence. + +1. **Row visibility must be occurrence-exact, not merely relation-valid.** `visible_row_relation` finds the relation's current source and then accepts any rendered row for that source. It is adequate for existing singleton removal, but not sufficient to validate `StateRow { placement, relation }`. A relation can survive while being reassigned to another State or while its owner's placement is replaced. The group predicate must require the specified projected placement, the relation's current source equal to that placement's element, and membership in that placement's currently rendered `state_action_rows` / `internal_rows`. Falsifier: preserve the relation UUID, reassign its source to another placed State, then deliver the old row snapshot; never rebound selection to the new row. Both row kinds currently lose placement at `hit` and right-click capture, so fixing only one path is incomplete. + +2. **Existing primary fallback is now stronger than the initial ordinary adapter.** Current `app/multiselect.rs` preserves the reconciled survivor when `held_primary || lost_primary`, regardless of a legacy semantic-to-placement remap. Keep this rule for State; do not restore the older `lost_primary && legacy.is_none()` condition. State additionally needs an exact row primary marker because both an internal row and an edge inspect a relation UUID. `behavior.diagram.placement` remains a node-inspection field; putting a row owner there would allow legacy Hide to delete its state placement. The typed selection is authoritative for rows, while the inspector still addresses the relation. Falsifier: remove a primary row's occurrence, keep its relation/model work alive, and retain an unrelated selected node; that node becomes primary with one coherent inspector, including after Undo. + +3. **Unchanged authored route does not imply unchanged rendered route or labels.** `translated_routes` changes only pins when all actual endpoint owners and any required boundary move together; it preserves manual attachments and `EdgeLabel` fraction/offset. An external route's stored record stays byte-for-byte equal, while its automatic path and computed label position may change as one endpoint moves. State action/internal rows follow their projected state, and `symbol_labels` recomputes collision-aware derived positions after routing. Do not add a second label translation or freeze their old screen positions. Independent assertions must separate authored `EdgeRoute` equality from rendered geometry and symbol-label ownership. Include guards and multi-line transition labels, a State action referencing work shared by another State, and an external route with a manually offset label. + +4. **Projected boundary geometry cannot be used as the movement anchor's original authored position.** EntryPoint/ExitPoint X is snapped to the owner boundary by projection; Y is clamped below the header and above the bottom. `StateCanvas` currently captures the original from `diagram.nodes[placement].position`, correctly. A reusable group helper initialized from `ProjectedNode.position` can introduce a large unintended first delta even with an unsnapped policy. Falsifier: stored entry position differs substantially from its displayed boundary position; drag by 7.25 world units and require precisely that stored displacement, with independent projected-boundary expectations. Preserve unsnapped State node movement; bend snapping remains 5 units and ordinary movement 10. + +5. **One shared plan is a contract, not sequential reuse of singleton movement.** `diagram::multiselect::group_preview` already consumes only `EditNode`/`SetEdgeRoute` members of the pure planner's Batch into a cloned diagram. State can reuse that small application pattern. Neither preview nor commit may loop through `state_edit::plan(Event::Move)` or `Command::MoveNode`: each sees an incomplete movement map and misses internal pins between unrelated roots. Recheck all selection members against one original snapshot before constructing the plan; commit once through Document. A refused/out-of-bounds member must leave every other member, source and history unchanged. + +### Smallest safe implementation split + +- **First: exact selection boundary and inspection**, including State context, row hit identity, primary reconciliation, passive modifiers, menu Add/Remove/dismissal and visible membership. Until movement exists, intercept group drags/Delete with an explicit full-set refusal so no singleton fallback is reachable. Test actual App admission before shared side effects, plus same-semantic replacement and row reassignment. This is useful foundation, not completed State group support. +- **Second: placement movement**, integrating unsnapped captured displacement, common atomic planner, diagram-only preview and paint-cache keys. Run independent whole-Project/source/history oracles plus mounted ordered-pointer/Inspector-relayout workflows. Re-run existing State bend/reconnect/attachment checks because hit-priority changes can regress them even without editing their planners. +- **Third: placement Hide and removal boundary**, captured-set Hide with one Undo/Redo, survivor primary and unplaced-picker recovery. Mixed rows/edges and semantic group deletion must remain explicit refusal until the separate aggregate impact-review path exists. A group menu cannot expose enabled singleton destructive actions during any split. + +Every split must reject pending property/source/State drafts without calling `apply`, and reject stale snapshots before status, camera, allocation or selection mutation. Advancing source history must retire old snapshots even if semantic/placement UUIDs are unchanged. Reuse existing ordinary stale-event loop for State Move/Hide/Collapse/Refuse with view, owner, tool, revision and selection-generation changes; add the row-source reassignment and entry authored-versus-projected tests above. diff --git a/harness/multiselect-union-source-20260923.md b/harness/multiselect-union-source-20260923.md new file mode 100644 index 00000000..f78f11b9 --- /dev/null +++ b/harness/multiselect-union-source-20260923.md @@ -0,0 +1,35 @@ +# Ordinary and State multiselect: combined source checkpoint + +This source-only merge combines candidate +`e34ba27e26ede3ab55f276acc250d92b2e0c3204` with delivered ordinary/State multiple +selection at `bed7f825fd00ec32299032a664a5f5d753aad86d`. Branch: +`codex/multiselect-union-956atf-20260923`. Integration ownership remains with +Codex-956atf in Gitea issue 27; issue 29 retains peer ownership of remaining +Process/Allocation/Scenario adapters and aggregate semantic deletion. + +The parent inspected and resolved 15 conflict hunks in seven files, retaining +the delivered multiselect changes within each hunk and all surrounding merged +code. This preserves State attachment/bend routing, exact row/menu targets, +the separate ordinary and State displacement policies, and the combined +Scenario/Text/Properties behavior. Both native multiselect examples now specify +`Diagram.layout: None` for the combined schema. Current shared instructions were +copied from default-branch instruction checkpoint eda2d0d; the protected user +application specification remains identical. + +The [independent source review](reviews/state-multiselect-merged-20260923.md) +found no blocking regression in its scoped event-admission, menu and canvas +review. It names the exact reviewed source hashes and discriminating combined +checks. Parent checked the resolved diff for whitespace errors and absence of +conflict markers. These checks do not establish compilation or runtime behavior. + +No Cargo build, test suite or native workflow has run on this combined source. +Peer evidence remains tied to its original commits. The earlier e34 desktop +validation proceeds against its separate frozen checkout; its eventual results +must not be relabeled as this merge's results. Combined source needs its own +actual test inventory, relevant core/desktop regressions, strict lint, matching +native evaluators/GUI and representative editing/history/save/reopen checks. +The source checkpoint is not integrated root acceptance or a release. + +Known peer limits remain explicit: shared picker Escape dismissal (issue 35), +reported multiline State-label overlap, no native SA/minimum-window/DPI coverage, +and remaining selection adapters. Linux/WSL stays user-deferred. diff --git a/harness/reviews/state-multiselect-merged-20260923.md b/harness/reviews/state-multiselect-merged-20260923.md new file mode 100644 index 00000000..8aceb646 --- /dev/null +++ b/harness/reviews/state-multiselect-merged-20260923.md @@ -0,0 +1,30 @@ +# State and ordinary multiselect merged-source review + +Reviewed the staged `.local/peerunion` source on `codex/multiselect-union-956atf-20260923`, combining `e34ba27e26ede3ab55f276acc250d92b2e0c3204` and `bed7f825fd00ec32299032a664a5f5d753aad86d`. The read-only e34 baseline was `.local/scunion`, **not** root production. Scope: merged App admission/reconciliation, menu dispatch, State/ordinary canvas interactions with newer Scenario/Text/Properties behavior. No source mutation, compiler, GUI, expensive Git scan or archived helper execution. + +**No concrete blocking regression found in this bounded source review. Suitable to freeze for combined gates; not combined-runtime acceptance.** + +- `app.rs:1382–1470` retains e34's complete early Text/source/allocation/Scenario admission block verbatim. New selection/state envelope checks at `:1350–1380` precede it and do not mutate state on refusal. Scenario revision/visit/epoch, separate camera lifetime, and Properties target/pending-input checks still run before reveal/allocation reconciliation. The new State singleton guard likewise refuses queued Select/Move/Remove before those shared side effects. +- `app/multiselect.rs:88–99` scopes ordinary selection through `route_context` and State selection through a validated `state_context`; it does not introduce a Scenario adapter. Reconciliation uses exact occurrence targets and State placement ownership (`:141–249`), preserving row identity rather than substituting its owner node. `app.rs:1529–1533` reconciles after ordinary dispatch; the allocation early-return path also reconciles (`:1483–1488`). Existing early Text implementation is byte-identical to e34 and retains its nested update dispatch. +- `model_menu.rs:99–121` validates the captured occurrence against visibility and request identity. At `:236–339`, group entries precede the State popup; the singleton State popup is still appended/reached. `app.rs:2613–2630` retains pending-draft refusal, dispatches group selection directly through `update`, and only uses legacy `select` for other menu actions. This avoids collapsing a group before processing its captured snapshot. +- `behavior/state_canvas.rs:220–235,1540–1544` preserves the meaningful distinction between canceling edits and canceling all gestures: dirty input retires model editing without clearing an ongoing read-only pan. Group currentness checks include selection snapshot, editability, tool and viewport (`:790–811`); retirement consumes the event before it can become a fresh keyboard edit (`:1528–1606`). The new selection route in `behavior/state_view.rs:43–46` still reaches App's early envelope, while other events retain State dispatch. +- Ordinary and State selection strips reserve 56 pixels (`views.rs:695`, `behavior/state_view.rs:248`); the State missing-element picker also reserves its 34-pixel slot (`:147–151`). This preserves canvas size/focus-tree continuity during group selection and Hide/Undo. Both new native example Diagram literals include `layout: None`. +- Direct byte comparison confirms `source_defaults.rs`, `source_project.rs`, both source-project implementation modules, `app/scenario_spacing.rs`, and `app/text_workspace.rs` are unchanged from e34. No writer/default migration was silently replaced by the selection merge. The newer Scenario picker guard and Properties action row remain in merged `views.rs`. + +## Targeted combined acceptance checks + +Reuse the delivered ordinary/State core, App and mounted tests, but run them on the frozen merged input set. In addition, retain the e34 Scenario/Text/Properties regressions so source-only preservation is exercised together with new reconciliation. The discriminating seams are: a captured State/ordinary group action after Text Apply or navigation; a same-visit stale Scenario action while a new reveal exists; a State mixed row/placement selection with a pending inspector draft; and group Hide/Undo while the missing-element picker changes contents. Require model/source/history, selection, camera and pending ownership to remain correct together. These are coverage requirements, not newly proven defects. + +Native held-drag cancellation and dirty-State read-only pointer pan remain unobserved unless actually exercised. Existing mounted/core evidence must not be relabeled as native closure. The prior ordinary preview-cache ordering concern remains a targeted redraw check, not an established stale-commit bug; this merge review supplies no new reachable failing trace. + +## Reviewed byte pins + +| File below `crates/oa-desktop/src/` | SHA256 | +| --- | --- | +| `app.rs` | `21fbca718bea0bf77414b18e0552768235c6f5b3af6f71a80b77a89521a59445` | +| `app/multiselect.rs` | `1b1c29c2153aa976272e38136433731b6c3b53f0610ab53f03341ec664f8b361` | +| `model_menu.rs` | `99a6792cb4187ec4fa9031005fb9bc2b03b41c70df00dffa9047a1dfd41304d2` | +| `behavior/state_canvas.rs` | `9ccf0682bf1b97db408662a750d9c29b627eb7dc51cb61056e11ceeac62baf88` | +| `behavior/state_view.rs` | `234ff0f4969c1e6067100795d1e75f5f6a7e8c22e374392becb83b23912f4f2d` | +| `diagram.rs` | `99d9ccd2ed0daf4716a36bfc1a22ce3caf6d5165bd30a3c1d8b21577b262783d` | +| `views.rs` | `c6a8e67e5a5cde7a2f31e4c5488980555d5a38d854bb60e1c9477688d719237c` | diff --git a/harness/state-bends-20260923.md b/harness/state-bends-20260923.md new file mode 100644 index 00000000..f8b702f8 --- /dev/null +++ b/harness/state-bends-20260923.md @@ -0,0 +1,90 @@ +# State transition bend editing — issue 23 + +Branch-specific work above attachment source `53d01a3`, on +`codex/state-bends-ytr83r-20260923`. +[Issue 23](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/23) +owns live scope and acceptance. This document records the design; it does not +assert completed verification or integration. + +The user asks to edit bends in State-machine connections as in architecture +diagrams. State already renders `EdgeRoute.waypoints`; its missing capability is +interactive authoring. Bends are positions in one saved diagram. They neither +change transition endpoints/meaning nor belong to every view of the transition. + +## Interaction decision + +Use direct manipulation of an already selected transition: drag an exposed line +segment to insert one bend, or drag an authored square handle to move that bend. +An initial selection click changes no route. Endpoint circles retain semantic +reconnect behavior; the separate attachment action retains its explicit mode. +State vertices and action rows retain selection/movement precedence. Clicking +a transition label selects it; this task does not introduce label dragging. + +The transition menu offers Add bend here when invoked on an eligible segment, +or Add bend at the longest interior segment from Actions. Right-clicking an +authored square offers Remove bend. Clear bends removes all authored waypoints +in this view while retaining both attachment preferences and the label override. +Existing transition Delete behavior stays explicit and separate; this design +does not add a pin-specific Delete shortcut. + +Alternatives considered: an Edit bends mode makes the distinction visible but +adds another persistent tool and cancellation/focus state; menu-only editing is +discoverable but too slow for repeated shaping. Direct dragging plus explicit +menu actions matches the existing architecture workflow without another mode. +The menu provides a recovery path when a label or endpoint covers a line hit. + +## Invariants + +- Only an actual visible, unsuspended routed State transition can be edited. + Internal transitions render as rows rather than lines. Folded endpoint proxies + suspend authored routing. In both cases existing dormant waypoints survive; + the UI disables operations instead of deleting metadata. +- Generated elbows, mandatory endpoint leads and computed self-loop detours are + routing output, not authored bends. Editing inserts one pin, never the entire + computed polyline. The picked rendered-segment index determines insertion + among stored pins, including reversed or overlapping traversals. +- Movement requires three screen pixels. Dragged pins snap to the existing + five-unit grid. A stationary press or return within three screen pixels + restores the exact baseline, including imported off-grid coordinates and + absence of a stored route. It creates no history entry. +- Preview uses the actual routing engine and captured baseline. The model stays + unchanged until release. A successful drag replaces only `waypoints` in the + current route, leaving attachments, label, relation metadata and other views + exact, in one existing `SetEdgeRoute` transaction. +- A gesture belongs to its document, diagram, revision, interaction epoch, + selected transition and viewport. Escape, pending edits, tool/context changes, + camera/bounds changes, focus loss, pointer exit, outside release and a pan/menu + interruption cancel it. A canceled preview must not supply menu pin indices. + A delayed drag result must not apply after selecting another object. +- Finite positions, valid indices and the existing 128-pin limit are checked at + the command boundary. Exact no-ops are suppressed before execution. + +## Verification plan + +Use real canvas event tests at multiple zooms for insertion/movement, ordered +cursor input, cancellation, exact return-to-press, preview invalidation and menu +targets. Planner/app tests compare entire Projects and source bundles across +Undo/Redo, preserve attachments/label/reference views, and refuse internal, +folded, nonfinite, stale, pending and capacity-invalid operations. Exercise +right-click during a provisional insertion and selection changes before queued +commit admission. Retain existing attachment/reconnect and focused Delete tests. + +Then exercise the actual Windows application: add, move, remove and clear bends; +check unchanged attachments and independent views; Save/Undo/Redo and reopen; +observe menus, guidance, preview and normal close. Bind evidence to an identified +source/binary and predeclared complete-Project expectations. Compilation alone +does not establish interaction acceptance. Linux remains user-paused. + +The first full workspace run exposed a read-only pan regression: canceling model +edits on every pending-draft event also canceled the middle-button pan. Keep +edit cancellation separate from navigation cancellation; the existing scoped +pan regression exercises this distinction. The self-loop test also distinguishes +typed-model restoration by Clear from byte-exact source restoration by Undo: +source-preserving edits may retain harmless empty-list whitespace, whereas Undo +must recover the original source bundle exactly. Failed run receipts are retained; +these findings do not assert that the corrected candidate has passed its gates. + +Codex-956atf retains issue 16 resize and issue 27 integration ownership. This +work uses the existing State model/router and needs composition with those +contributions. It does not certify a combined release, change Process/Scenario +notation, redesign automatic arrangement or adopt a new routing schema. diff --git a/harness/state-multiselect-native-design-20260923.md b/harness/state-multiselect-native-design-20260923.md new file mode 100644 index 00000000..5ad0a8c1 --- /dev/null +++ b/harness/state-multiselect-native-design-20260923.md @@ -0,0 +1,43 @@ +# State multiple-selection native fixture and invariant evaluator + +Implementation candidate: `crates/oa-core/examples/state_multiselect_native.rs`, based on the existing ordinary fixture CLI. Written on branch `codex/multiselect-state-9e8r6t-20260923` after `7e348d9`. Compilation, unit tests and native inspection have **not** been run for this candidate. Parent owns these gates and a fresh oracle review. + +## Entry and targets + +The generator refuses an existing output directory and writes `baseline.json`, `manifest.json`, and canonical source under `source/`. All object IDs derive from the fixed UUID-v5 namespace `c1192026-0923-5000-8000-000000002929` and the aliases in the manifest, except `state_view`, fixed to `00000000-0000-0000-0000-000000290101`. It is the smallest diagram ID, and is a dedicated State-machine view rather than Architecture. The fixture does not persist a transient selection; select A, then Shift-click B in the native application. + +| Alias / target | Authored world geometry / purpose | +| --- | --- | +| `node_a` / `state_a` | A – Processing: (0,0), 340×450; click header near (40,35) | +| `node_b` / `state_b` | B – Ready: (410,30), 210×150; click header near (450,65) | +| `node_c` / `state_c` | C – Complete: (410,330), 210×140; must remain fixed | +| `node_inner` / `inner` | A nested Region: (25,220), 290×200 | +| `node_leaf` / `state_leaf` | A.1 – Classify: (60,290), 180×90 | +| `node_entry` / `entry` | Authored (-210,280); State projection attaches symbol X to A's left boundary | +| `node_exit` / `exit` | Authored (870,350); State projection attaches symbol X to A's right boundary | + +The outer machine/Region are unplaced in the main view to keep these targets readable after Fit at a 1442×932 native window. Readability is a proposed layout until visually verified. World header coordinates are not screen coordinates; account for the actual camera and canvas origin. + +A and C contain StateAction rows referencing the same `shared_work` Activity. A has the `refresh` InternalTransition row. The Activity has a refinement trace, including recorded baseline properties, to an unrelated SA Function. A second State view and SA view provide preservation sentinels. Main-view routes have authored hard pins, explicit attachment sides/fractions and relative label offset/fraction; `between` also has a guard and multiline label. + +## Expected mutations and limits + +`moved` infers a common displacement from the saved A placement. The generated baseline puts A at authored (0,0), so its saved f32 position is the original f32 delta without lossy subtraction of a nonzero anchor. All main-view placements and route pins were translated equally by (-30,-100); sizes, relative geometry, route label offsets, other State/SA views and IDs are unchanged. Use that externally pinned origin baseline for moved checks, not an already-moved baseline. Positive command candidates include (0.01f32,10) and (0.1f32,-0.2f32) to expose nonbinary-delta rounding regressions. It independently enumerates exactly six moved placements: A, B, nested Region, nested State, entry and exit. It translates only `between` (A→B), `entry_route` (entry→leaf), and `exit_route` (leaf→exit) pins. `external` (B→C) pins remain exact. No production movement, descendant or routing helper contributes to the expected Project. + +The delta must be finite, have length at least 0.01 and each axis at most 300 world units in magnitude. Fractional values are accepted without a 10-unit grid requirement. This is a bounded native-evidence acceptance threshold, not a new movement/product limit. All comparison is complete Project equality after explicit edits to a baseline clone; JSON/source roundtrips are also required. + +`hide` removes exactly `node_a` and `node_b`, leaving their descendants' placements, all authored routes, engineering objects, shared activities, StateAction/InternalTransition records, other views, opposite-layer objects and refinement baseline intact. `unchanged` requires complete model equality. Authored external route equality does not require its recomputed path or labels to stay at identical screen positions. + +The checker accepts JSON or `.arcadia` inputs, rejects source errors, and allows ordinary engineering-completeness warnings. Pin the generated baseline, manifest, generator source version and generation command in an external evidence receipt **before** native interaction. Project identity alone does not authenticate the baseline. A moved output passing this checker cannot prove pointer intent, focus, stationary-click behavior, selection paint, exact original source bytes or single-step history; observe those independently in the native workflow. + +## Parent verification commands + +```text +cargo test -p oa-core --example state_multiselect_native +cargo run -p oa-core --example state_multiselect_native -- generate FRESH_DIR +cargo run -p oa-core --example state_multiselect_native -- check unchanged FRESH_DIR/baseline.json FRESH_DIR/source/project.arcadia +cargo run -p oa-core --example state_multiselect_native -- check moved FRESH_DIR/baseline.json SAVED_MOVED_JSON_OR_ARCADIA +cargo run -p oa-core --example state_multiselect_native -- check hide FRESH_DIR/baseline.json SAVED_HIDE_JSON_OR_ARCADIA +``` + +For Hide performed after movement, use the externally pinned moved snapshot as that operation's baseline. Unit positives use the validated production command only to generate candidates; the oracle remains explicit. Negative tests require valid-model corruption where applicable and cover missing/double movement of each descendant, internal/boundary pins left behind, external pin drift, attachment/label metadata, StateAction phase, internal guard, shared work, fixed C, other views, trace metadata/baseline, destructive Hide and malformed source. Source-file positives and valid unexpected source edits exercise the same complete-model check. diff --git a/harness/state-multiselect-review-20260923.md b/harness/state-multiselect-review-20260923.md new file mode 100644 index 00000000..5777e2f5 --- /dev/null +++ b/harness/state-multiselect-review-20260923.md @@ -0,0 +1,27 @@ +# State multiple-selection independent review + +Scope: State adapter above `7e348d967d1787c052a145b7448636613a6a7054` on `codex/multiselect-state-9e8r6t-20260923`, subsequently committed as `2a3b9ba822a49ead233c4b5f98412a07c1c1da12`. This is branch-specific review evidence, not integrated acceptance. The reviewer was a fresh GPT-6 Astra agent and did not author the adapter or checker. Parent owns verification and integration. + +The read-only review covered App admission, exact State row identity, context-menu ownership, the unsnapped gesture policy, diagram-only simultaneous preview, fixed State controls, and the independent native checker. No Cargo or native GUI execution was performed by the reviewer. + +## Findings + +1. **Checker rounding false negative.** Recovering the displacement from A's nonzero `(30,100)` origin can round it differently from the original `f32` displacement. For actual `(0.01f32,10f32)`, saved A's X is `30.0100002289`, giving recovered `0.010000229`; an inner node initially at 55 actually becomes `55.0099983215`, while replaying the recovered delta predicts `55.0100021362`. The reviewer reproduced this with single-precision arithmetic. The strict whole-Project oracle should retain exact comparison and use an origin anchor, with nonbinary fractional positive cases. This is a checker defect, not evidence of incorrect product movement. + +2. **Late refusal changes unrelated input state.** Same-context legacy State `Select`, `Move`, or `Remove` events under a multiple selection were refused in `edit_state_diagram`, after `App::update` had reconciled pending reveal and retired allocation input. Static tracing showed that the rejected event could clear navigation focus/pending reveal, advance allocation epoch, and clear `arrange_next`. Refusal must occur before those hooks. Model/source/history-only assertions do not cover this failure; the regression must include the shared state and a queued `Select`. + +The reviewer found the inspected exact-row visibility, typed menu target checks, planner-backed preview, authored boundary-symbol anchors and fixed-height control design consistent with the intended contract. This bounded review does not establish correctness by itself. + +## Required verification identified by review + +Run current-State App and mounted focus tests, existing State bend/reconnect/attachment tests, and singleton route interactions with `StateCanvas.selection = Some(singleton)`. Legacy canvas constructors using `None` bypass the new selection admission path. Verify actual native group movement, Hide, history, source preservation and reopen on the recorded Windows build. Linux verification remains user-paused. + +## Follow-up review and first corrected gate + +The same independent reviewer inspected both corrections and found them addressed statically: the main fixture positions, pins and recorded header points shift consistently by `(-30,-100)`; A is at the origin; `(0.01,10)` and `(0.1,-0.2)` are real-command positive cases. The legacy State rejection now precedes the shared hooks, with a regression comparing valid navigation markers, full selection, model/source, camera, epochs, feedback and history. Three singleton-selection route tests cover snapping, commit/cancel, stationary reconnect and Shift priority. No new blocking finding was reported. A minor fidelity note remains: the legacy queued Select probe uses a placement UUID rather than the semantic UUID normally emitted by the canvas; the admission rejects the whole legacy variant independently of that UUID. + +Parent Windows `core-2` ran all 11 State native-checker tests successfully with 733 unchanged captured inputs (input-map SHA-256 `9cf85879dfb0bb5ec3b15c66b23df316e6bfe8451b6cf0c895bf7b9b5fbdde4a`). This includes the new nonbinary fractions. Full `desktop-2` passed 1,286 tests with six existing ignored on the same inputs, now compared exactly to committed Git blobs at `2a3b9ba`. Native acceptance remains separate. + +Strict Clippy then found the optional exact menu target enlarged the message enum. Commit `3480b54201a5f361cca0bcd0d45dd6546a7a7bc9` boxes that field without changing its matching rules. Strict core/desktop all-target lint, formatting and all 122 focused State/menu tests pass on 735 unchanged inputs exactly equal to that commit. The revised capture scope explicitly includes the existing attachment and textual-authoring compile-time fixtures outside the usual source directories; historical receipts remain unchanged. Native verification remains separate. + +The subsequent [native result and limitations](multiselect-state-20260923.md) record parent execution and the critic's independent screenshot/receipt/source-history review. No new blocker was identified for the State contribution. Shared picker Escape behavior is tracked as issue 35; multiline transition-label overlap and the distinction between recovery-choice visibility and actual recovery through the picker remain explicit limitations.