diff --git a/.gitattributes b/.gitattributes index 66fa63f6..a1a8b3c1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -61,3 +61,4 @@ harness/experiments/scenario-native-3c9e6ee-20260911/** -text whitespace=-blank- harness/experiments/scenario-native-3c9e6ee-20260911/**/*.arcadia whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol harness/experiments/analysis-codec-windows-20260911/** -text whitespace=-blank-at-eof,cr-at-eol harness/history/2026-09-11-ledgers-before-gitea-migration/** -text whitespace=-blank-at-eol,-blank-at-eof,cr-at-eol +harness/experiments/canvas-union-windows-20260911/** -text whitespace=-blank-at-eof,cr-at-eol diff --git a/harness/STATE.md b/harness/STATE.md index be18e10a..cf8d6bdc 100644 --- a/harness/STATE.md +++ b/harness/STATE.md @@ -11,8 +11,8 @@ Completed Linux evidence remains historical; the VPN remains untouched. ## Verified implementation -Production source **3c9e6eeb64ce3efc03b29890b82199a42c21f335** on -`codex/behavior-authoring-20260910` passes720/720 Windows desktop tests and strict +Previous native acceptance uses source **3c9e6eeb64ce3efc03b29890b82199a42c21f335** on +`codex/behavior-authoring-20260910`. It passes 720/720 Windows desktop tests and strict all-target desktop Clippy. It includes direct scenario/process authoring, parent-focus and editing-session/chooser corrections, and boxed workflow events. Evidence checkpoint e7491fb preserves exact logs. Instruction-only merge828c50c @@ -22,31 +22,34 @@ was read without overwriting the user's local specification. The matching GUI built warning-free and passed recovered SA State, clicked Reuse, and ALT Fragment creation with exact Save/Undo/Redo and final native Open/Save at -the1100x700 client minimum. The whole Project, all identities and both source files +the 1100x700 client minimum. The whole Project, all identities and both source files match every required history checkpoint. The native session is closed. - [Windows regression/lint evidence](release-chooser-correction-gate-20260911.md) -- [Current native SA evidence](release-scenario-native-3c9e6ee-20260911.md) -- [Earlier native OA/SA Action evidence, scoped to80cf197](release-scenario-native-20260911.md) +- [Previous native SA evidence](release-scenario-native-3c9e6ee-20260911.md) +- [Earlier native OA/SA Action evidence, scoped to 80cf197](release-scenario-native-20260911.md) - [Scenario oracle and reproducible procedure](fixtures/scenario-authoring/README.md) ## Isolated contributions and material limits Core candidate **847b1caa7e75bb82f6ce5193c45bcfc45973ad1a** is on the separate `codex/analysis-core-956atf-20260911` publication branch. Windows execution passes -the128KiB-thread lifecycle subprocess, all6 codec tests,28 analysis tests,26 numeric +the 128 KiB-thread lifecycle subprocess, all 6 codec tests, 28 analysis tests, 26 numeric tests and strict all-target core Clippy. These counts are separate scopes; the -subprocess driver is included in the6. Production `DEPTH_128_VALIDATED` remains +subprocess driver is included in the 6. Production `DEPTH_128_VALIDATED` remains false. Legacy/migration/save integration, release memory measurements and complete source8/transport/evaluator acceptance remain open. [Windows codec evidence](release-analysis-codec-windows-gate-20260911.md). -The isolated R123 canvas source has no remaining material finding in its bounded -[independent review](release-behavior-canvas-r123-review-20260911.md). -Its17 production mounted and2 shared tests are still unexecuted and it is not in -the accepted GUI. Parent/child native focus, the complete direct-manipulation -matrix, persistent process placement/lanes and coherent B4/source integration are -not established by the scoped SA creation test. +The integrated R123 canvas at **ff0175488ac57a92d401b00a0e4542582a82d8e9** passes +729/729 desktop tests with a warning-free compiler, including the 17 new mounted +cases and preserved parent-focus/session gates. The guide refresh at 82fd0ac +passes 3 guide tests. Strict Clippy at abffa4b identifies four obsolete canvas +variants; no post-cleanup or current GUI pass is included in this checkpoint. +[Closed gate and exact evidence](release-behavior-canvas-windows-gate-20260911.md), +[independent union review](release-behavior-canvas-union-review-20260911.md). +Parent/child native focus, complete direct manipulation, persistent process +placement/lanes and coherent B4/source integration remain unverified here. Complete quantities/calculations/constraints, maintained libraries, graphical/ source/REST parity, transitions and engineering-task usability, aligned reST/help, @@ -55,8 +58,11 @@ prototypes and published previews are intermediate results. ## Authoritative records -- [Live priorities, dependencies, ownership and migration record](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1#issuecomment-412) +- [Project overview and historical coordination](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/1) - [Direct process/scenario canvas work](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2) +- [Activity allocation drag](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3) +- [Analysis, codec and source/API integration](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/7) +- [Maintained reusable libraries](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/8) - [Decisions and reusable evidence](DECISIONS.md) - [Shared overview](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/wiki/BehaviorAuthoring20260911) - [Published milestones and their exact limitations](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/releases) diff --git a/harness/experiments/canvas-union-windows-20260911/candidate-commit.txt b/harness/experiments/canvas-union-windows-20260911/candidate-commit.txt new file mode 100644 index 00000000..3822b047 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/candidate-commit.txt @@ -0,0 +1 @@ +fb1da912c9951f33feed2fad9ad689bc46cd20d0 diff --git a/harness/experiments/canvas-union-windows-20260911/clippy-82fd0ac.log b/harness/experiments/canvas-union-windows-20260911/clippy-82fd0ac.log new file mode 100644 index 00000000..4b465c5f --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/clippy-82fd0ac.log @@ -0,0 +1,26 @@ + Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop) +error: this `if` statement can be collapsed + --> crates\oa-desktop\src\behavior\canvas_workspace\control.rs:158:13 + | +158 | / if matches!( +159 | | event, +160 | | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +161 | | ) { +... | +169 | | } + | |_____________^ + | + = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#collapsible_if + = note: `-D clippy::collapsible-if` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::collapsible_if)]` +help: collapse nested if block + | +161 ~ ) +162 ~ && !layout +163 | .bounds() +... +167 | state.focused = false; +168 ~ } + | + +error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 1 previous error diff --git a/harness/experiments/canvas-union-windows-20260911/clippy-82fd0ac.result.json b/harness/experiments/canvas-union-windows-20260911/clippy-82fd0ac.result.json new file mode 100644 index 00000000..f98c1085 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/clippy-82fd0ac.result.json @@ -0,0 +1,7 @@ +{ + "source": "82fd0acf64d87f322e12602154f14a565b29de4c", + "start": "2026-09-11T18:06:04.2394485Z", + "end": "2026-09-11T18:07:10.7060485Z", + "exit": 101, + "command": "cargo clippy --locked -p oa-desktop --all-targets -- -D warnings" +} diff --git a/harness/experiments/canvas-union-windows-20260911/clippy-abffa4b.log b/harness/experiments/canvas-union-windows-20260911/clippy-abffa4b.log new file mode 100644 index 00000000..7fae4df8 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/clippy-abffa4b.log @@ -0,0 +1,44 @@ + Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop) +error: variant `First` is never constructed + --> crates\oa-desktop\src\behavior\scenario.rs:99:5 + | +98 | pub enum CanvasEvent { + | ----------- variant in this enum +99 | First(Context, Tool), + | ^^^^^ + | + = note: `CanvasEvent` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis + = note: `-D dead-code` implied by `-D warnings` + = help: to override `-D warnings` add `#[expect(dead_code)]` or `#[allow(dead_code)]` + +error: variant `Canvas` is never constructed + --> crates\oa-desktop\src\behavior\scenario.rs:133:5 + | +132 | pub enum ScrollArea { + | ---------- variant in this enum +133 | Canvas, + | ^^^^^^ + | + = note: `ScrollArea` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis + +error: variant `Tools` is never constructed + --> crates\oa-desktop\src\behavior\scenario\workspace.rs:10:5 + | + 9 | pub enum Shelf { + | ----- variant in this enum +10 | Tools, + | ^^^^^ + | + = note: `Shelf` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis + +error: variant `Tools` is never constructed + --> crates\oa-desktop\src\behavior\topology.rs:62:5 + | +61 | pub enum Shelf { + | ----- variant in this enum +62 | Tools, + | ^^^^^ + | + = note: `Shelf` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis + +error: could not compile `oa-desktop` (bin "arcadia-studio") due to 4 previous errors diff --git a/harness/experiments/canvas-union-windows-20260911/clippy-abffa4b.result.json b/harness/experiments/canvas-union-windows-20260911/clippy-abffa4b.result.json new file mode 100644 index 00000000..78cadce8 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/clippy-abffa4b.result.json @@ -0,0 +1,7 @@ +{ + "source": "abffa4b3f0a859d7439103100f59805804d46e55", + "start": "2026-09-11T18:08:58.7613771Z", + "end": "2026-09-11T18:10:49.3969111Z", + "exit": 101, + "command": "cargo clippy --locked -p oa-desktop --all-targets -- -D warnings" +} diff --git a/harness/experiments/canvas-union-windows-20260911/compile-inputs-06ec962.json b/harness/experiments/canvas-union-windows-20260911/compile-inputs-06ec962.json new file mode 100644 index 00000000..db9cdefa --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/compile-inputs-06ec962.json @@ -0,0 +1,1407 @@ +[ + { + "path": "Cargo.lock", + "bytes": 119973, + "sha256": "065C23C94908FE499D32ED91D55A70946ED5B08ECFDA3B483EC1E635572257BF" + }, + { + "path": "Cargo.toml", + "bytes": 400, + "sha256": "369F5B4B2B60705A94623C8C0B68F8E0B048F263280CE07AE0629AD353797D62" + }, + { + "path": "crates/oa-cli/Cargo.toml", + "bytes": 279, + "sha256": "49C567F1AF74A826683B4D96845FEB3806FD9BCB7DC2767E16B5509D4E5E1CDE" + }, + { + "path": "crates/oa-cli/src/lib.rs", + "bytes": 15498, + "sha256": "97BEBAB63B4A611A1D36AD0C20FE452CBE1985FE3A6FBD39C954603DA11410FC" + }, + { + "path": "crates/oa-cli/src/main.rs", + "bytes": 2984, + "sha256": "DE964F9B6C59C0F31C080AFBD3B31D442B07B1B44F6A187A248D3409C45D35FB" + }, + { + "path": "crates/oa-cli/tests/source_cli.rs", + "bytes": 16600, + "sha256": "A3A1B856C7A9FD3D80BC8074150A24FBB0EF0E9DDF736C7CEA036725FC3DC2B8" + }, + { + "path": "crates/oa-core/Cargo.toml", + "bytes": 408, + "sha256": "6B5C062474EBB62D5082E07DABED99EE85E4A833EDF26690FB4F887028D3416F" + }, + { + "path": "crates/oa-core/build.rs", + "bytes": 45, + "sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9" + }, + { + "path": "crates/oa-core/examples/generate_demo.rs", + "bytes": 795, + "sha256": "E26F432C95331AECA50985DBB3AD2C558AFFD43023B585811F0F7BFAA59F6661" + }, + { + "path": "crates/oa-core/examples/generate_release1.rs", + "bytes": 1798, + "sha256": "1D5F11165D74893E4BC18DBF3A2F2111F4026BD89FD06EE7F0BAE69E45C4D4B5" + }, + { + "path": "crates/oa-core/examples/release_dense_views.rs", + "bytes": 14664, + "sha256": "A83158488EF9B7394F136C52B1496719BE935C4E5BDFE457CAB5C9894F5F9412" + }, + { + "path": "crates/oa-core/examples/routing_workshop.rs", + "bytes": 17248, + "sha256": "571307B18B6D4FA04700C5A4D4B0C2154C419E93A21292B2028E7B6F46A2FAA7" + }, + { + "path": "crates/oa-core/src/analysis_numeric.rs", + "bytes": 3735, + "sha256": "6C88EEF3119162AEB6C07B9E75812C3AF9FBFE75554B0503AD00F103CB478408" + }, + { + "path": "crates/oa-core/src/analysis_numeric/dimension.rs", + "bytes": 3045, + "sha256": "D4AD8E5307A1620E31791D724A0E31FDBA860D00DC83E3E1F2209D9C5E22A340" + }, + { + "path": "crates/oa-core/src/analysis_numeric/exact.rs", + "bytes": 17338, + "sha256": "4EB8CB0F66B10E5DA204899AF82EE26ADFEC74BAB47B1A6C9842B5488EE04C4D" + }, + { + "path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json", + "bytes": 6085, + "sha256": "3B959DEC5E100A6F03817F5314BEF497347BA579EBD235476516D4BD83399181" + }, + { + "path": "crates/oa-core/src/analysis_numeric/meter.rs", + "bytes": 7129, + "sha256": "1F7174BEB992935B4C399ACDEF91114AF73FAF041070FEAB968455AC827E8EE4" + }, + { + "path": "crates/oa-core/src/analysis_numeric/quantity.rs", + "bytes": 12257, + "sha256": "EAD5142D6EEEA8F99399D57E02D8F7D819980923DE76387CD454694D6A747AEE" + }, + { + "path": "crates/oa-core/src/analysis_numeric/tests.rs", + "bytes": 27486, + "sha256": "323CE82FC057D36F44463569FD8EA324C7D700FDDFBF32BCE15E04BD2A66970B" + }, + { + "path": "crates/oa-core/src/data.rs", + "bytes": 42696, + "sha256": "7F9C7C7DD8353CB5A38597B92CDE6D3E890203D392473187EB387D41FF37414A" + }, + { + "path": "crates/oa-core/src/demo.rs", + "bytes": 20657, + "sha256": "AD62123F96C20FF023CAFC1A9849B0FA06BC62EDD1FB9701799E4599B458600C" + }, + { + "path": "crates/oa-core/src/language.rs", + "bytes": 99938, + "sha256": "638F08A42E9CF4996B5D07F69B4D6F517A518369F3604D25BE1006E40E080765" + }, + { + "path": "crates/oa-core/src/language/semantic_source.rs", + "bytes": 7326, + "sha256": "584B5BB59D87A7B8D44D5DD2D124C3A1323B21492B138A17BC9526C0087C9E29" + }, + { + "path": "crates/oa-core/src/language_nesting_tests.rs", + "bytes": 20639, + "sha256": "BD061611E8CB6F28AB203C73E544AC98BF275EB464CD314AD5FBD25F773DF33B" + }, + { + "path": "crates/oa-core/src/lib.rs", + "bytes": 549, + "sha256": "EA1D9BF5ACBA8381BA204DB12FF5B5FE1F320417D17AF517E46B90FD6BCB241C" + }, + { + "path": "crates/oa-core/src/migration_v2/README.md", + "bytes": 1515, + "sha256": "FB61D3FAA33D7BDFBA73415415A0882C0FBD3E0B45DF42F972A19AEC6DA927B9" + }, + { + "path": "crates/oa-core/src/migration_v2/adapter.rs", + "bytes": 9490, + "sha256": "66741B4C8354A23B8A932ECC5BF1BC3BB6E74279558757B8C35BC2157A7E51B5" + }, + { + "path": "crates/oa-core/src/migration_v2/mod.rs", + "bytes": 1158, + "sha256": "857D8BB13B6814DABA6780CA770C16352943C01CB130245D055F11B3B957B3E9" + }, + { + "path": "crates/oa-core/src/migration_v2/model.rs", + "bytes": 52059, + "sha256": "8AC63EB59B2E57BDECFF2477EE0634096090924D3DD1851E1C454880A889DACB" + }, + { + "path": "crates/oa-core/src/migration_v2/references.rs", + "bytes": 13369, + "sha256": "33A6819ABC83139FDAEBEEBD1B553238A328637106CCE7DD5A535F1791D43E20" + }, + { + "path": "crates/oa-core/src/migration_v2/source.rs", + "bytes": 21946, + "sha256": "74FEE9B24E7C912182C998AB9D9AEEC125E2009C3214959229E844C67AFE2BF6" + }, + { + "path": "crates/oa-core/src/migration_v2/state_validation.rs", + "bytes": 15769, + "sha256": "E29EF071BAB0A68460DDF55C46131C6F66C022D52C3C602C37C013C3649DA1B0" + }, + { + "path": "crates/oa-core/src/migration_v2/transitions.rs", + "bytes": 704, + "sha256": "F74174C532BA56B57C80B719A11FB4ADEA0A78CCC2828FFF72FE2B9EDA5A10B1" + }, + { + "path": "crates/oa-core/src/migration_v2/validation.rs", + "bytes": 98736, + "sha256": "6D5E4A4E2805AE5045BF789A0485E14A61C63346587BACE2B4A91173A688F780" + }, + { + "path": "crates/oa-core/src/migration_v3/README.md", + "bytes": 2128, + "sha256": "3755A63F840D7F0EADF68AFA7A9D587877A9468FF982D3610539E8A0BB067A82" + }, + { + "path": "crates/oa-core/src/migration_v3/mod.rs", + "bytes": 23023, + "sha256": "F4EDB6A0B50ECFE8670973776D703047B064420CB1F05913129C00FC656B3722" + }, + { + "path": "crates/oa-core/src/migration_v4.rs", + "bytes": 4020, + "sha256": "50C652964F0F309E7F3F54B6EF12C40D42FCECC0490F005995C713DF1179AF70" + }, + { + "path": "crates/oa-core/src/model.rs", + "bytes": 66352, + "sha256": "4585C895C64C4A183941C92CF047A9E52FF0CBE41971E16E78D229EA9DC101FB" + }, + { + "path": "crates/oa-core/src/operations.rs", + "bytes": 11405, + "sha256": "A5FFBBF54CD440316A106D1188BFB699B57DAE59F466EDCB33618C333CD19F41" + }, + { + "path": "crates/oa-core/src/persistence.rs", + "bytes": 31774, + "sha256": "1C01474F906A63E552C0D590B272C95821C00A100B3E79E58E67DBBEAC341A14" + }, + { + "path": "crates/oa-core/src/ports.rs", + "bytes": 45413, + "sha256": "5CAC12EEA70A54EBEBBD6BE77B63CE92E3195325C4BC7D27705C518686CBAC80" + }, + { + "path": "crates/oa-core/src/projection.rs", + "bytes": 21374, + "sha256": "69ED12BACE2CC5024E151752A174AB14034AC2D05EB741FCF13E9A27521C059B" + }, + { + "path": "crates/oa-core/src/projection/port_layout.rs", + "bytes": 13474, + "sha256": "A08D17D10A58BB8B206DD001B19B91D5AA3A9F743B298A4DA57D503223C20079" + }, + { + "path": "crates/oa-core/src/projection/state.rs", + "bytes": 5233, + "sha256": "73A607BAC713EC7FF49A509FEFB1A53C7686DCA93EE241FFC835564D743AFB28" + }, + { + "path": "crates/oa-core/src/references.rs", + "bytes": 21957, + "sha256": "93553C234F32DA35F1F41A7B83AD81C471C9803EFA78CA222CF7DE3C2F86C0BB" + }, + { + "path": "crates/oa-core/src/routing.rs", + "bytes": 50309, + "sha256": "C5EBB350BA76F731F5C48D968BFBBA4B6EF68EC046EB5893D116C4637EF15080" + }, + { + "path": "crates/oa-core/src/semantic.rs", + "bytes": 540, + "sha256": "F9895DD5B8FB27D10D4CFBB41C3542EF2018365A589B8D5778E5674AE5A1F239" + }, + { + "path": "crates/oa-core/src/semantic/catalog.rs", + "bytes": 49407, + "sha256": "1FF74FCCA18084D178681D7FF38EE1CCAC632C27C4DE0595BA1CEAB8067AB3A4" + }, + { + "path": "crates/oa-core/src/semantic/historical.rs", + "bytes": 15271, + "sha256": "EAF3284BF234312698514CDB636BC8348E1B5D746CB16B18DF777E77D312ABFB" + }, + { + "path": "crates/oa-core/src/semantic/linked.rs", + "bytes": 6035, + "sha256": "6F335B14E809AECFC35DEFCAF77CB821B080D1506DBC46297912B25AF98C07A2" + }, + { + "path": "crates/oa-core/src/semantic/mutate.rs", + "bytes": 22683, + "sha256": "DC2B4C4A6EB156FE468EAA8C85FD37A505C0CD8EA1F226CB69CD01689AFD3D9D" + }, + { + "path": "crates/oa-core/src/semantic/review.rs", + "bytes": 51116, + "sha256": "0C157D2C327E72E89CCA3B423E71EE73798105C5BB7271439C1A101642C91E3E" + }, + { + "path": "crates/oa-core/src/semantic/types.rs", + "bytes": 10405, + "sha256": "6922FE35089F12FAD43AB0CE1C6886D01A3855F90B8E2543E99802AC5E6F1BBB" + }, + { + "path": "crates/oa-core/src/session.rs", + "bytes": 35237, + "sha256": "1EB77B329B2986FC6F5DC0171E77FD860375CDFD8C769A1367B445D6C49EBE00" + }, + { + "path": "crates/oa-core/src/source_files.rs", + "bytes": 74803, + "sha256": "669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683" + }, + { + "path": "crates/oa-core/src/source_project.rs", + "bytes": 61843, + "sha256": "1394370BCC5908873B830C2C11312E6E1611AF41F46C4F43CA0A6BCB713BB466" + }, + { + "path": "crates/oa-core/src/source_project/source_migration.rs", + "bytes": 4700, + "sha256": "ADD463AE85C420F853D324B00A221F0F874164CC3F3ED6BBB07557A9ED7A8C95" + }, + { + "path": "crates/oa-core/src/source_project/source_structure.rs", + "bytes": 20225, + "sha256": "9F1F83315856A7025FE662C0FBB13452F31613224A03663150D6225D1DDC162F" + }, + { + "path": "crates/oa-core/src/state_validation.rs", + "bytes": 15665, + "sha256": "E28D7F74385E2A78763716B34491BD73A00BFECCC085970CEB130627E3FAEE18" + }, + { + "path": "crates/oa-core/src/topology_context.rs", + "bytes": 13734, + "sha256": "16DE69B297DE95D93C73808621607833B713AF811DD082F0169186AE1E5C3E2D" + }, + { + "path": "crates/oa-core/src/topology_context/analysis.rs", + "bytes": 20036, + "sha256": "DEAD6194F0AA60CC46045C0E5EE10DBDBB689158C38634DFBBBC45D2BA88AC65" + }, + { + "path": "crates/oa-core/src/topology_context/analysis_tests.rs", + "bytes": 5258, + "sha256": "17839263410815E96FE027D4EBA3AAE0A9AB3B82B16947E2B29EBE0D09782B56" + }, + { + "path": "crates/oa-core/src/transitions.rs", + "bytes": 51998, + "sha256": "FADD1B07B89CAE4117B91ABB4F1AC9EA81FF818EF20F2175E76EE8CF21FB708D" + }, + { + "path": "crates/oa-core/src/transitions/communication.rs", + "bytes": 8634, + "sha256": "2B476630A88EDD9021B9156CA12A5402E01604095F0D4002AFB452671903A028" + }, + { + "path": "crates/oa-core/src/validation.rs", + "bytes": 112792, + "sha256": "7C1B4AE5E4EBBA13E99AF9158CCDE138DFC5E9A7AFB580E76FBA72D0B64BA74E" + }, + { + "path": "crates/oa-core/src/validation/coverage.rs", + "bytes": 6478, + "sha256": "B08E4B344ECAD055714168C83BEFF7FE29BA4EDC53EDBFFA2EAE04E5580A4D5F" + }, + { + "path": "crates/oa-core/src/view_options.rs", + "bytes": 24481, + "sha256": "421EDE0A04C593CA1DF0E6E0D06FABCFCC8113CDBCFB0ADC7F5C8EF4D1724208" + }, + { + "path": "crates/oa-core/src/view_options_tests.rs", + "bytes": 21433, + "sha256": "64AAC44DE2DF42A1012239A3DD75C473DAF0C8990B3170B7BF7E78A556080208" + }, + { + "path": "crates/oa-core/tests/communication_boundary_scenarios.rs", + "bytes": 8816, + "sha256": "E1100B8F363910A487D90C73ACC7DF543DC6D532158C3C63AD11CD3D40D8E12B" + }, + { + "path": "crates/oa-core/tests/communication_means.rs", + "bytes": 18102, + "sha256": "A25F2749180BC6F4BEB1445BFAD3351EEDF0669131A7439B633AB804439E95A4" + }, + { + "path": "crates/oa-core/tests/coverage_diagnostics.rs", + "bytes": 20527, + "sha256": "E2E9C3996DAAB47F2BD23F5D8C48B4E372AF33331152F5FC85B76F06D04FCDF4" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia", + "bytes": 29220, + "sha256": "F0DF129C1914BFC01D0B047B0CD4B520D8CF0C249EA9CED085DE400C0BE87FF3" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json", + "bytes": 43990, + "sha256": "3ED53508EB544BBD788832F7BEBEA2EC31B3D66191F24F45098CC9750EC4C005" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia", + "bytes": 2450, + "sha256": "305F19D1B79F99F8D4CF627BA2E9CA6836BB7959CC0070227815937A699EAFEA" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.json", + "bytes": 8240, + "sha256": "1B41299C53204E6232397D25A58D5BCCDBBC16E770C75E8EDB55CE36CCFE58A2" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia", + "bytes": 1222, + "sha256": "5FA53A26A5AD010C8E59EB56D9B2B52003569E66C0377FFB58840590168AD0C1" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json", + "bytes": 1734, + "sha256": "2808088AFCC165F4BD6DA5129151143411C997041C3DE61D600AB76E05D24A47" + }, + { + "path": "crates/oa-core/tests/language.rs", + "bytes": 56482, + "sha256": "77F6CB2A69976BFB5F60B243B71C07FB0EDC7505002C1E9E73FCD6DDF6D9496E" + }, + { + "path": "crates/oa-core/tests/port_anchors.rs", + "bytes": 26893, + "sha256": "EFBC0FA18733B1AAAC501F9785A4C392FC49C05C45A5778D26E9B5DD951CA6FA" + }, + { + "path": "crates/oa-core/tests/port_migration.rs", + "bytes": 15620, + "sha256": "1801C376A5435AC7EFB2A8342348B2C7FFEF930BBA65DECA4ACB66B06E0483D7" + }, + { + "path": "crates/oa-core/tests/port_source.rs", + "bytes": 4858, + "sha256": "8D2DDACA4045D330F6EDA2591561497B858AAD53769A823249D0BEA4F5916E12" + }, + { + "path": "crates/oa-core/tests/prepared_commands.rs", + "bytes": 9555, + "sha256": "52CAB1C668064777412DF0A6A3C20D74D579B33794B4B00525B9F151C06EAC9B" + }, + { + "path": "crates/oa-core/tests/process_diagnostics.rs", + "bytes": 1104, + "sha256": "1DBBC590239EF7542BE4A2BDC9543AAA64D108417B03DD312A0894E364745906" + }, + { + "path": "crates/oa-core/tests/r3_constructor_ack.rs", + "bytes": 24163, + "sha256": "E5C44676EA0AAB97F2E091DDAC2F4BC4A4A90F5877F7BBF08B61229600944DD0" + }, + { + "path": "crates/oa-core/tests/r3_migration.rs", + "bytes": 6590, + "sha256": "EBD04120F5215B3956FDC79EE78F6E38734C7D13392F1FC28AABA957912265BD" + }, + { + "path": "crates/oa-core/tests/r3_semantic.rs", + "bytes": 77473, + "sha256": "49C3763EAF2131070A7C46CC23A84F917D292D41610B6404510E5607F8890C32" + }, + { + "path": "crates/oa-core/tests/r3_source.rs", + "bytes": 15311, + "sha256": "F31F60659DF3C7EF601650D1CFF140589D8729F01A3F65A4A6A6C2272F15C408" + }, + { + "path": "crates/oa-core/tests/r6_data.rs", + "bytes": 41437, + "sha256": "5F7FFB3BC73C3C85423623FE56F8680646DC97040327BDBEAD9D47A37E80F155" + }, + { + "path": "crates/oa-core/tests/r6_migration.rs", + "bytes": 9174, + "sha256": "81519BC1512F5E069432E9A1CDAD04C10216D492DF01B4B75B02102480EBD6A1" + }, + { + "path": "crates/oa-core/tests/r6_source.rs", + "bytes": 9519, + "sha256": "88A0AA087A7678E2898FF6057538072CA92072190712F05FFF08372CC56EBED6" + }, + { + "path": "crates/oa-core/tests/release1_teaching.rs", + "bytes": 10010, + "sha256": "BE179290D871125DC97CFD2A3067C7AC89C8A538FDB0AFAFC5B825BCDFB2BEF2" + }, + { + "path": "crates/oa-core/tests/release_capabilities.rs", + "bytes": 19422, + "sha256": "B92CBA2A562E78287DDD7DC3E94557848D17FAA0C8A102601B50286B9AF48A30" + }, + { + "path": "crates/oa-core/tests/release_diagrams.rs", + "bytes": 7689, + "sha256": "CEFCC0582D91266D5DE6EE5DAEFCFE41B606AB971AB235B59034A8E1DBC24B7D" + }, + { + "path": "crates/oa-core/tests/release_language_limits.rs", + "bytes": 5204, + "sha256": "8578DA4F76BBD2D0E1BFE295AB1259E2162A3FE3D0D2BA8901915D7A40942806" + }, + { + "path": "crates/oa-core/tests/release_ports.rs", + "bytes": 32740, + "sha256": "506E62BEAB0C7EC0CECE83E9E0C1A810FA1F99285C9E1CAB0C77F06DE55A40E1" + }, + { + "path": "crates/oa-core/tests/release_routing.rs", + "bytes": 23165, + "sha256": "AE168FAABA23769B7CA1FD65F217B9DB671726E67276EB5A04E5E72B1447A462" + }, + { + "path": "crates/oa-core/tests/release_scenarios.rs", + "bytes": 41067, + "sha256": "0F5D876BADEEC653FC7B12610A77D867E9A53582E605253DEFF8D379DA21572F" + }, + { + "path": "crates/oa-core/tests/release_states.rs", + "bytes": 26873, + "sha256": "B1A26ACEE95EABA7F9920F9DA738A1E33606552EB078E6D298895DAFF7E3D811" + }, + { + "path": "crates/oa-core/tests/release_topology.rs", + "bytes": 17643, + "sha256": "C51DBE5AA5A038F9ED830DAA02AA67714472179EB7FD6DA5CE36DC6931C8B3A9" + }, + { + "path": "crates/oa-core/tests/review_regressions.rs", + "bytes": 21823, + "sha256": "F154E5C88D35E31512E2B07733A1327EA3E2FCFD1EF8EB23066A4E9573F387C1" + }, + { + "path": "crates/oa-core/tests/route_source.rs", + "bytes": 7270, + "sha256": "1A73FDBAC0D0AE0E84D8D67B6AD398E11223958F5DCA007F73757C91827DECFB" + }, + { + "path": "crates/oa-core/tests/routing_geometry.rs", + "bytes": 31890, + "sha256": "2D8EEA4A2BBA535ED0EA6DAABCCDAF87B375828C404BAA10518C7BD7FD6253D3" + }, + { + "path": "crates/oa-core/tests/routing_labels.rs", + "bytes": 5530, + "sha256": "DFF884D799C3C5C9257210248C55767E294E3CB317C605CBD0DDC9600C216EB6" + }, + { + "path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs", + "bytes": 4640, + "sha256": "D93EAF9667F4221232168BEF0FE9F069E5BE1B1F23410BDA5DD67010B96E5C8A" + }, + { + "path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs", + "bytes": 14957, + "sha256": "7F526D363BA05E7AC89FAF0FEAD0796E419C34EE204EDF44A1B46C78308B0970" + }, + { + "path": "crates/oa-core/tests/source6_view_contract.rs", + "bytes": 14307, + "sha256": "FA566DE53946D40BBBD9033EDFEED177180C4DC44AFA2F2F4667FFDC622EB3BB" + }, + { + "path": "crates/oa-core/tests/source7_contract.rs", + "bytes": 43262, + "sha256": "CAD4F1A135DDBD947BDF3C71D1B44F1E438725095EA8C4D893464BC721C2363F" + }, + { + "path": "crates/oa-core/tests/source_files.rs", + "bytes": 13168, + "sha256": "F0429A0B89A443458EA9AF7083C1E4E7411417A7E6B47E618BFBB9266DC164F9" + }, + { + "path": "crates/oa-core/tests/source_identity_updates.rs", + "bytes": 5139, + "sha256": "F52AA38B4D317E532C641AD7463472E4B40E802E60052CF0017556FFC63DA06C" + }, + { + "path": "crates/oa-core/tests/source_nested_updates.rs", + "bytes": 16892, + "sha256": "9AC3390936C0D250651EA94A1B8FAF0ADFB680F061ECECC56F2A52909B5BA855" + }, + { + "path": "crates/oa-core/tests/source_project.rs", + "bytes": 45910, + "sha256": "CEF58ADB16AA4D2527DDC0628AEC3E26F0B42B4BE70014A7E360FAFDAC13A942" + }, + { + "path": "crates/oa-core/tests/state_diagrams.rs", + "bytes": 23973, + "sha256": "6B02B387A3AE559CB3AD72DE24292A5ABCB76AAAC0ADBAB5D2AA5211B22CD2E5" + }, + { + "path": "crates/oa-core/tests/workflows.rs", + "bytes": 33426, + "sha256": "BD843DFDDBD5724EC08F657571F887496203056FE2FA5864EF50A168A613B89B" + }, + { + "path": "crates/oa-desktop/Cargo.toml", + "bytes": 842, + "sha256": "F3547B2B924D5DB6706C0A8E069014B440FF65315D82FC8ACED0360AF4B3CDC9" + }, + { + "path": "crates/oa-desktop/build.rs", + "bytes": 45, + "sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9" + }, + { + "path": "crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "AA2E4853B25BB7E185B1DE49EDA42028523C8EBF9599BFE6B7CB2A2B2A688090" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring.rs", + "bytes": 27835, + "sha256": "37C7DB33E99DD4ADCC5A51F09EE4878AF94FD32413359890A541690080885F65" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/palette.rs", + "bytes": 8887, + "sha256": "8F9937CB30B48B83759FD084A8A5990A2DAA3920280F1210ABE048560241985A" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/tests.rs", + "bytes": 14732, + "sha256": "ED864839940A1910C97537B6EE151B57AB115ECE6F60BAE602552B84F9AF4A5D" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring_tests.rs", + "bytes": 18599, + "sha256": "77EB1A0038D038B989F7AC568760E16FE23C4B8D73CD9FBE32B70817EA23CFEC" + }, + { + "path": "crates/oa-desktop/src/app/behavior_lifecycle_tests.rs", + "bytes": 13669, + "sha256": "1AF2E290AA259A6AF6B0585950CDA93FE3A562C7B8BE1E938FE1D80747B11808" + }, + { + "path": "crates/oa-desktop/src/app/checkpoints.rs", + "bytes": 29576, + "sha256": "55E5BABDCABEF767AF3BC8BE9CC3153B3F5CC0695701B890820EC0225643852E" + }, + { + "path": "crates/oa-desktop/src/app/diagram_selection_tests.rs", + "bytes": 5582, + "sha256": "4FA507ADBC23DB3A6EB202D809C695EC0EBD6181356930C4FA7528A9A0C7BE57" + }, + { + "path": "crates/oa-desktop/src/app/hosted.rs", + "bytes": 25059, + "sha256": "2F7094C078E1D7CBCCD6C0D310AFA6C400A7E031F12918BBFCE65BA62754E316" + }, + { + "path": "crates/oa-desktop/src/app/hosted/tests.rs", + "bytes": 30344, + "sha256": "FA118F64D04B400CCC01E4E67ADAF3541975288882AFA265A9E7F092CDBDA232" + }, + { + "path": "crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53D4A422D3066E07CCCB9E8EF2AD08EA95E451B9B7CBEF29A27E6634E035ED97" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38E033E23E8135032F70117A376A5FE7BC68127C466559A35D7EC200932E3F77" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests.rs", + "bytes": 20067, + "sha256": "B96DDC6BFF7F8FDE97720E18A92070F891C1340348D2E19C7C9C5A7BDA650CE8" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests/mounted.rs", + "bytes": 32805, + "sha256": "14A4914A1C86E50C395672DAB8FE80D7AF679A79C9082890B509FAD7B7C543D9" + }, + { + "path": "crates/oa-desktop/src/app/review_navigation_tests.rs", + "bytes": 7049, + "sha256": "872FA91479DAD5436E139FFCAECE270323E9F5DAEEB7A4D80C2C2534F48BC21B" + }, + { + "path": "crates/oa-desktop/src/app/save_as_tests.rs", + "bytes": 4113, + "sha256": "F8B915793F1A36FD152569C4781A4BDC17F4AF404A8CEB373330053413087A1C" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram.rs", + "bytes": 9979, + "sha256": "F7FD50CC9A86ADB794713958348AF63AFC7C698F74965E13188B7B8A9E735C55" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram_tests.rs", + "bytes": 39957, + "sha256": "612E414B1A2CAB12CBFD8FCE23745559FF27577700694D53AF67AD1F2883EE2C" + }, + { + "path": "crates/oa-desktop/src/app/transition_context_tests.rs", + "bytes": 11508, + "sha256": "47002FB025F3A2F8C53871F9509BFE8611A61DB6FB0E1B3125EB6E875425CB1D" + }, + { + "path": "crates/oa-desktop/src/app/view_copy_tests.rs", + "bytes": 7846, + "sha256": "296821C51DCDE568E7D717039AA26BFC1B56A9074AA16051007BB351B33A4D44" + }, + { + "path": "crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5E7CECB54BFCA64E4462F7881F460900ABD925BA2CFE9451CE2B0C78AF4BC850" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7744, + "sha256": "096D667C4C8EE3732D52668A9562C665F2D9A192ABDA4E5487FCAD7621C8DBFD" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7860, + "sha256": "851AF1E9B155D6D0D079D9164C5616FADC33E961391A6604A43EABF5158957B3" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "CC58CB3838F546150D22FE4DD83A3FFD52504C279FEBAEB8D0B4A6A3B8A73950" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 27020, + "sha256": "D84D022574A6F1D69F203D9383502227FB9FC23605C6FB48779CA9F09BEE546D" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "AF1C7C1E802D1207EE6C7BD66B703811A250D7B56511512AF6C0945C776E68A7" + }, + { + "path": "crates/oa-desktop/src/behavior/contextual_source_tests.rs", + "bytes": 49386, + "sha256": "5F05318A6533503B9D6FEBF1EC4DD7256F192394BC1B952A7904C8175BD8DDAE" + }, + { + "path": "crates/oa-desktop/src/behavior/creation.rs", + "bytes": 28978, + "sha256": "33AE9AAF4BA90DE9432870CC8E99708EF39E3638AE8283A9253C333785C91299" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/app_tests.rs", + "bytes": 13181, + "sha256": "E9C5BF2B07357B7A2F679AE0D8F6904C1C1461AAEB94FA0DB478B54D4641AE3E" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/tests.rs", + "bytes": 22793, + "sha256": "A0D1DAF9FB3920F7E6181A9802D84FBE056CBF0043B9E41DBA5FDDA687D9818A" + }, + { + "path": "crates/oa-desktop/src/behavior/form_session.rs", + "bytes": 3941, + "sha256": "F5027FB3A5A5599A182E7D1A10DBA56DD66EB1F23A4BA4C4A1744C9A6A5B6E61" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "FB1DA03E8DF05A59253F7FDF34391E30ABD8AEA427B7D0A12B86B80FF3BF9ECA" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/focus.rs", + "bytes": 9728, + "sha256": "73A9EA10AD7312086081B8688396A21AB4251C9FAECC2DF6D984B702F3963533" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284F5A7DA09BDC77D70C6788A091D57927150C796FFC10D786793CFF7DA4B7C6" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950CF4347A3EDEA615B1AC71D9A681C32B446B5109FCFD0F03F3AE2B147360BF" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle.rs", + "bytes": 9324, + "sha256": "758D244C2E8DE61F515D540BAA4D967275F49720E0253C6C9159EB9ACD71C859" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle/tests.rs", + "bytes": 15430, + "sha256": "01B576993B329810CD78A859B45AC52C66F5A14549065661DD1308196E0B724D" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_operation_tests.rs", + "bytes": 10087, + "sha256": "89FB71C690E07EAC314F7A5E05862D5C1E2F9C21039673BF22C4AC894D21C977" + }, + { + "path": "crates/oa-desktop/src/behavior/source_draft.rs", + "bytes": 18031, + "sha256": "08190823090BD8B0305809099ABADCC0DE431175F7044F1D5ABDCEDA52612EE1" + }, + { + "path": "crates/oa-desktop/src/behavior/state_canvas.rs", + "bytes": 93561, + "sha256": "2E2AD57B86F4CC1CC0C4C4C230571EF19BCC2F052001A8BDCC44925E5103077A" + }, + { + "path": "crates/oa-desktop/src/behavior/state_edit.rs", + "bytes": 35736, + "sha256": "CB2289E727163626152FC2F11195C6EE824851CB9442C57496DA97FE7AEBFB7C" + }, + { + "path": "crates/oa-desktop/src/behavior/state_menu.rs", + "bytes": 7447, + "sha256": "CC87C5CAA177E114E4D9A7F7367E37EC2B0D1E1C16B0B99709B05EA0E59CF894" + }, + { + "path": "crates/oa-desktop/src/behavior/state_view.rs", + "bytes": 6988, + "sha256": "1EB892A9139ECB0671784C879EE5114F27E598E5B3589F5CDAD0ABFFD5909F24" + }, + { + "path": "crates/oa-desktop/src/behavior/states.rs", + "bytes": 26986, + "sha256": "8986ED9492960E20F1254E9DFE3FF91D79CE9FEF66F3BDA738F9933278043954" + }, + { + "path": "crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723E51A7DA4EB224D51C1659EBBFA4DC8B11557BC8BF70F6E3E1649BAD24449" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5D0AC5B3E258FC70987A00EDE1E603681D99746596F2FCC46AD2B3667D65203A" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual_routing.rs", + "bytes": 16731, + "sha256": "D914B0E82AA8D253FE64BE2EF74443D1FD2E6CEC26D1CF30620A57646E1D426C" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309B8978EC2586FF4614236C2F624AF6D934D117363E917B8C1CF9F2B1295EB" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733F8BADAFB9BA87E3F861F03F78809450EEE72CC118341EB03B24904790144A" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "A990947CB39B2FB15C21DFBC95EE16F92BF6BE749D3872C6904762B9BDE37089" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "B2D502C11426BFFB8763515F2A56AA4C9F8E203BB36D65EB34F81C123FC406A6" + }, + { + "path": "crates/oa-desktop/src/connection_edit.rs", + "bytes": 5301, + "sha256": "020581E077F82B43C936B623CC51733B02B88428DF4E5F25DF74B9705819FA37" + }, + { + "path": "crates/oa-desktop/src/connection_tool.rs", + "bytes": 27023, + "sha256": "649DA93677D52DAE495B0E19BC32F3122AB05A6D84DE8402578FF5CE3F6EE1D3" + }, + { + "path": "crates/oa-desktop/src/context_menu.rs", + "bytes": 52955, + "sha256": "629354B34DC232763700251F2695B4D4E53D6251C0B1432ECC0DFE424F7DFEAC" + }, + { + "path": "crates/oa-desktop/src/data_values.rs", + "bytes": 23877, + "sha256": "E1DEB1CAD551F0A9FDEF9653AAD257CFB36E6D9C0F93B6245061EC3FAB8FD365" + }, + { + "path": "crates/oa-desktop/src/data_view.rs", + "bytes": 38871, + "sha256": "158B852BE5D3DB450AA5CDA8B68A35BD96BDDCE1D8D053A7A23582095C06362D" + }, + { + "path": "crates/oa-desktop/src/diagram.rs", + "bytes": 105256, + "sha256": "4580B170E264D14E5CFA874709F6D2E076A05155BFBE1809336A9D05F75E98EF" + }, + { + "path": "crates/oa-desktop/src/diagram/lifecycle_tests.rs", + "bytes": 9316, + "sha256": "D0E72CC0574657D5A31C3CE307B58401D0ED77EEDCCD1AA02377F55AB0BC03F0" + }, + { + "path": "crates/oa-desktop/src/diagram/pointer_tests.rs", + "bytes": 11833, + "sha256": "10CA5049034159205AC8F22805354F308B0BA0596F331568C26CD1393BC8CEDC" + }, + { + "path": "crates/oa-desktop/src/diagram/port_hit_tests.rs", + "bytes": 5624, + "sha256": "D2D0647629BF725B4DDDFC211F5A27D495ED093A79AF87CDAE6C196F09A97B93" + }, + { + "path": "crates/oa-desktop/src/diagram/port_labels.rs", + "bytes": 13408, + "sha256": "28782983B82E4FCA5DD4410AC8BD562359943B7010C7B24ECC42F0C7AD7FB63E" + }, + { + "path": "crates/oa-desktop/src/diagram/view_options_tests.rs", + "bytes": 2815, + "sha256": "CF0C98A7715558BAF17B59F67A80556F98AD30262B4434635E617C6601775433" + }, + { + "path": "crates/oa-desktop/src/document.rs", + "bytes": 31080, + "sha256": "2ECC2CD496D4A83FD7A32B9DC68E49F0F1CB31A06263DFD6939B4764DA5B73E2" + }, + { + "path": "crates/oa-desktop/src/document_save_as_tests.rs", + "bytes": 5781, + "sha256": "83DC243A25F4E97674FF0D86C794A490CC923E98F1F9FB05BB038F622F11E20B" + }, + { + "path": "crates/oa-desktop/src/explorer.rs", + "bytes": 14764, + "sha256": "FA01C59219564BA9AE5553ABC2B6F94E9849F9148D5812EF99B40318FE5DCF9A" + }, + { + "path": "crates/oa-desktop/src/explorer_view.rs", + "bytes": 13773, + "sha256": "A720D7484BCC3A24729332A00E005D1CC6F5F945A9CC3965134E0024DFFB581F" + }, + { + "path": "crates/oa-desktop/src/forms.rs", + "bytes": 105544, + "sha256": "EA124F479FCD2D3978AC3CB8EB81FEC4A7AAE5E4F4414C4BDD54849E954DEDF9" + }, + { + "path": "crates/oa-desktop/src/forms/context_fields.rs", + "bytes": 10831, + "sha256": "EAACD474F6500DD344D5A8B63CCB1D1DD86FE2AEA78AB708AFA8A80D62CE69BF" + }, + { + "path": "crates/oa-desktop/src/forms/data_fields.rs", + "bytes": 49285, + "sha256": "A540A0D451BFF56A96B1D28643B2E8C0649F3846350C25C5B154729C0538E77C" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields.rs", + "bytes": 27414, + "sha256": "E7100474CDE18BDFE95562BCB3C055F29B4B15AA64788A578194464AEBCD046E" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields/tests.rs", + "bytes": 33950, + "sha256": "16D3C308138C063F45986D53C621AD83C7DC2CE853EDD31B7204E2939A382DF9" + }, + { + "path": "crates/oa-desktop/src/forms/port_fields.rs", + "bytes": 30554, + "sha256": "3FE684D651D5BE6203FD9DB3979C772913E1AF5AC1514E67BE386FFD88DC8127" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser.rs", + "bytes": 12130, + "sha256": "B60AF363BE333AF048859E51C28E8C989CB5C95D696CAD16C8EEC15F78D57A12" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/adapter.rs", + "bytes": 10447, + "sha256": "87AB4BC3BF27FD4B5A458791CF62C7BA8E44E9EBF846D8B851B8B6FFB454CD64" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/tests.rs", + "bytes": 27835, + "sha256": "8E98F8A3FCB814D4A201CC85B3DB87A689FD3D2CE487F8A2FE312FC296F93377" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger.rs", + "bytes": 13336, + "sha256": "34D0ACECFE6BED193970AA473CC3313984971264B86D7B362A6B84C23D013C0A" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger/focus.rs", + "bytes": 9923, + "sha256": "F1E79F4B03B2F9FD1CDC335054D4AE177DC9E759B6EA3D334B2BCCA39B57191B" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/widget.rs", + "bytes": 33804, + "sha256": "D66CC17AF7A48BB8598BE683F084BB08FA7AF7304F9636F0A1BEA94C5CF6C727" + }, + { + "path": "crates/oa-desktop/src/forms/view_fields.rs", + "bytes": 24987, + "sha256": "5184D438F0DFD4E29F284FF048E19DA73E71BE1F24A6E9F123C7E922AECB85FA" + }, + { + "path": "crates/oa-desktop/src/guide.rs", + "bytes": 113998, + "sha256": "EE57F8847FF1692AC566D19A66CAA7BB77AD49B5F4E30C562B93117C59BC2671" + }, + { + "path": "crates/oa-desktop/src/highlight.rs", + "bytes": 2751, + "sha256": "DA7C254FFF7052E5FD9EC7469F473B91A819406028D7C304F6E22C4169561600" + }, + { + "path": "crates/oa-desktop/src/home.rs", + "bytes": 17587, + "sha256": "F03C1071334894F586A5A7C86026602A694DB3C2FAF902822E1DBE5156A859C2" + }, + { + "path": "crates/oa-desktop/src/home/tests.rs", + "bytes": 8167, + "sha256": "3B03CCFC97DFB75954E1E0ED568BB87A632295C07B96CEDC05ED381F87B378F3" + }, + { + "path": "crates/oa-desktop/src/hosted/client.rs", + "bytes": 21912, + "sha256": "54B24B4B50F8D87D7C92946EA5A2CFF4C98760E70555B3CE3D90BE27115E95D4" + }, + { + "path": "crates/oa-desktop/src/hosted/client_tests.rs", + "bytes": 23009, + "sha256": "3EF70907CA75E098D631C41B90BADD4CFF1E1EB1B3C78E15A9F811C13BA8FFBE" + }, + { + "path": "crates/oa-desktop/src/hosted/mod.rs", + "bytes": 3744, + "sha256": "1B75E11C80FCB38FD20BB3487E3F88729FDAC73E471C5AE64DA28C249F77C63B" + }, + { + "path": "crates/oa-desktop/src/hosted/view.rs", + "bytes": 9281, + "sha256": "FC0C7B7CE3A5F05C49713DF6661067751F81E810AF11C614CA265EA9490E6463" + }, + { + "path": "crates/oa-desktop/src/main.rs", + "bytes": 1511, + "sha256": "CB0A919FD0DF0924591283DA5B55D64D525D8461640441509350BE840235E51A" + }, + { + "path": "crates/oa-desktop/src/model_menu.rs", + "bytes": 34680, + "sha256": "12994A5433A91F8295A59D4BF40F35F1EDB8D8DBB497A11EAE8760299627F1D4" + }, + { + "path": "crates/oa-desktop/src/navigation.rs", + "bytes": 8839, + "sha256": "71355A12E1B4791106426D83FE5EBF0B29BCE8E1DC594F0655AB85695469669E" + }, + { + "path": "crates/oa-desktop/src/port_edit.rs", + "bytes": 7809, + "sha256": "70B99F577C4FDEDA8780C8DC1EE7B1639FD77AACA06658F06D725139FB3EFB6D" + }, + { + "path": "crates/oa-desktop/src/port_preview.rs", + "bytes": 35855, + "sha256": "069487C4ECE737D52E8AE4A84DCFB132236ACA6799E841C91C8E7244320CF871" + }, + { + "path": "crates/oa-desktop/src/port_workflow.rs", + "bytes": 40582, + "sha256": "CCC15F97C3272145A67BB5857B4FAEAF7437561BD61912FA27AB26E8824146A0" + }, + { + "path": "crates/oa-desktop/src/recovery.rs", + "bytes": 21041, + "sha256": "57E22E5FBB0E994E64AE03E69450271FE9F04A951DB1670C2A018ABE99DB4489" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/README.md", + "bytes": 1587, + "sha256": "A90002DF1E087D2DAC7BE26926864243568A0DE7C4103E4927435B34178E8016" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/profiles.json", + "bytes": 473, + "sha256": "2C12AE96B50E6DA5D60CE2B7FA00204BE71DFBE2FD51311FC337A0DE6A4332FF" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-bundle.json", + "bytes": 80974, + "sha256": "12715995E6F7F35414AEA53B3AD720B9B31873FFDEAF4444231DB1A5AC3C463E" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-model.json", + "bytes": 86191, + "sha256": "5535A706DF9EC21715E8B6A5AA6DBD79636AE461450AEE61305559016167612B" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-bundle.json", + "bytes": 85286, + "sha256": "EA88890A33F47D67DFB06D6DBBCC179058CF28DAD76AE7D615C00AAF30DBBBFE" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-model.json", + "bytes": 90629, + "sha256": "5BB8DB0B6394276666323483F977AC3586F88554C81A148C6D508FCA0EF4576F" + }, + { + "path": "crates/oa-desktop/src/recovery/source5.rs", + "bytes": 6956, + "sha256": "0F0A763F0C98C2C908D4C2B95D8A4C042B6A255EE1CC16260E5404669691F642" + }, + { + "path": "crates/oa-desktop/src/recovery/source5_tests.rs", + "bytes": 12146, + "sha256": "CFD17A742F1F27B523FCE7CD9309F0A07A30413395F6C58659FB91D02834A7A3" + }, + { + "path": "crates/oa-desktop/src/recovery/source7_tests.rs", + "bytes": 18213, + "sha256": "7EE61DE66C75BF949489BA51DDC9677E2B4EFC68801CF27909C9B894BFDC9288" + }, + { + "path": "crates/oa-desktop/src/route_edit.rs", + "bytes": 2941, + "sha256": "457AE59C2761A43A8620B4D336A7CF0F54B965997110ECAF968756A88B6B0374" + }, + { + "path": "crates/oa-desktop/src/semantic_review.rs", + "bytes": 3432, + "sha256": "8DD1E93783D63A663C7E56C28775A5C389CFD7A6B562013D6A95C891AFA21ED8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts.rs", + "bytes": 24201, + "sha256": "A79236A8150597FF808597E59CF4F1E9E61C74C4901D9C8CF42D3427BB52A9A8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs", + "bytes": 11034, + "sha256": "3F255D04E4BFFA5A467B75E3CBF6F7149AA3269A02F2A89FBAA1DB0E5DF9AE62" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_tests.rs", + "bytes": 20798, + "sha256": "04D0DD2AC453705185663F705282AA4A3A31433466A5BE305764248DA1A03C32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_view.rs", + "bytes": 6447, + "sha256": "2100FF73565765D8293C84F6CE2D8E566610C1524C50E32CE3C37DC33B6686E4" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments.rs", + "bytes": 18160, + "sha256": "DE0EC0C740895B160FF5B847332C087C3F1A48931583464069470CC61315ED59" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_tests.rs", + "bytes": 29389, + "sha256": "8B26BE0324E0F33B1E7CF7AF340350DF8ED98963002D6DA8589EE0E6F6900BEA" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_view.rs", + "bytes": 3032, + "sha256": "932F231554D77C7CB119D33BB06B9879DB5E12127AED187E67013E77B6458E78" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch.rs", + "bytes": 34485, + "sha256": "A462EB627AE19348072775CF8E60B17D62F6B682B74CC413906D493FBA1F0B3C" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch_tests.rs", + "bytes": 27546, + "sha256": "B67C4D7DCABAC710CF3D30730A8DB323180A558A415E2D117C868CDFA4804619" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose.rs", + "bytes": 27155, + "sha256": "C6ADCC9882DDE65E9F04F6EDA7F0C855C5CBB285CF421123029169112793CE9B" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs", + "bytes": 9809, + "sha256": "440DAF8D43278EF981C620D1818D0DAAED8FB83193B8E8FF67B2E42FBF882938" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructor_tests.rs", + "bytes": 54132, + "sha256": "85149521AD7DF62918F6F2FAB6DA2B3346C342AB4BCE7100DE3F6206C1E81096" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructors.rs", + "bytes": 12258, + "sha256": "0982163439B06A42D9AF1643530364109282E63F05B323A35A76D8241F3CAA32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/history.rs", + "bytes": 3211, + "sha256": "DF038C2464EAB8093A983F3104F0D04CA8F602010A50E1D3EEB8E53F2309E201" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed.rs", + "bytes": 14175, + "sha256": "BDD6C0FDC7108056BC5D7433254503449EED2209813680E717EAD24B9D0CE834" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows.rs", + "bytes": 13222, + "sha256": "4B7BF25806471AD22D89253B9AF8000F5ACFF53B4CC0AA1C1A83138E7B270ECA" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs", + "bytes": 30804, + "sha256": "1A3B09E7ED100411CDF8043F9A78DEFC664A873EE0201117709185C201A5C57F" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_tests.rs", + "bytes": 31163, + "sha256": "9DC9B989E5A9D408427CA1BAF86B2454F9DD3BAF40A551C70CA2359DC18A1227" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind.rs", + "bytes": 789, + "sha256": "4ED19952EA511D1B3226564A138E3322F994371D6B49794C14BC3689D1393E7F" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind_tests.rs", + "bytes": 23424, + "sha256": "5FCF9930E949AEBEB12EAB0327264F32A2388D62DBF098BD2365711E22623C52" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered.rs", + "bytes": 9705, + "sha256": "6A1ED894D1B9D0110BC784A75C49444F1D14F1F74620D701F22203B2D8D0A813" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered_tests.rs", + "bytes": 22383, + "sha256": "5C8EA3A69E3DA3B94D13F50F12AA7DC53246CCBA25FFCACE9552A0EB76B0BF36" + }, + { + "path": "crates/oa-desktop/src/semantic_review/recovery.rs", + "bytes": 28769, + "sha256": "88E69044D90C481593BAB7CC125F0401C05202FCCDDE07698CEC9483947FE8BE" + }, + { + "path": "crates/oa-desktop/src/semantic_review/row.rs", + "bytes": 38159, + "sha256": "B08FDC503C6C4D2FF99ADAC7E18C9BAA861BF86155E00B55D1E492430980D0F2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/scalar.rs", + "bytes": 15153, + "sha256": "9AD202174934DD9A63FBF9CBA43135AEAD571DB60846C8B7B1D3019A11DF26C2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/source7_tests.rs", + "bytes": 11665, + "sha256": "5E8221D77B068FA30C1571988448CB8829711735881D810F3D333CBC95D34A9F" + }, + { + "path": "crates/oa-desktop/src/semantic_review/tests.rs", + "bytes": 32480, + "sha256": "077EC6501DFAAB0B05EA3987943B204F08352952296D313555051D998CCD232E" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view.rs", + "bytes": 57626, + "sha256": "7CC7DD213AB01DADAD5DEBAEE868C30A1099466602869A799EBDF9B680E8EAF0" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs", + "bytes": 4737, + "sha256": "6C766B29C8664D15CB113D2CC71BBA9B60AE4CCF7E44C9C5D3C41E1DC4B7ED90" + }, + { + "path": "crates/oa-desktop/src/source_navigation.rs", + "bytes": 3938, + "sha256": "C1DA10882F35F8EB0675E68C642C9A209DDE09679CDED414CFB9903A4E49EC77" + }, + { + "path": "crates/oa-desktop/src/source_tree.rs", + "bytes": 21816, + "sha256": "4D55E0110A1EB128A5D26C28F3AAE5853D79B7245812B7C82BD275F38440A5AA" + }, + { + "path": "crates/oa-desktop/src/source_workspace.rs", + "bytes": 15522, + "sha256": "30F63A6B8D65A38D66061F90D864B5746CBACC0FBD65ABC028A8F3071C47D358" + }, + { + "path": "crates/oa-desktop/src/style.rs", + "bytes": 3015, + "sha256": "05D61BEC16141612CCAFA0988E8C4827F41754509EFDB3B961EAB69127DED896" + }, + { + "path": "crates/oa-desktop/src/tests.rs", + "bytes": 58557, + "sha256": "B017218195EEA2C96ABA12EBA8A99A38945C8CA910A652E272D34883BDB98691" + }, + { + "path": "crates/oa-desktop/src/view_copy.rs", + "bytes": 9043, + "sha256": "5630F31C942E1226AAFD67EABDACE6A5C5494875BDC6F7F077019E8110BA41A9" + }, + { + "path": "crates/oa-desktop/src/views.rs", + "bytes": 48347, + "sha256": "7148E01E8479C2294093159969BC33B9FF0CF916799D481681811C4DE5C60E53" + }, + { + "path": "crates/oa-desktop/src/views/traceability.rs", + "bytes": 7501, + "sha256": "4C89DCF35C25E737D8CCE4D543A5989CA104A5678E6CF40DF47E457773D2EF94" + }, + { + "path": "crates/oa-desktop/src/workflow.rs", + "bytes": 82963, + "sha256": "D9E7B11A86FD3EC79D91F0C9BC9CEBD5A2043CDAA7261622454FB7730AD8E060" + }, + { + "path": "crates/oa-desktop/src/workflow/communication.rs", + "bytes": 11084, + "sha256": "34D121AD97706012722E4BB6210072ECC3A5B4718B0CEAB0C3D0F14D4CD780A1" + }, + { + "path": "crates/oa-desktop/src/workflow/recovery.rs", + "bytes": 9859, + "sha256": "0849B0F34DBA5E8761BE5FA05784D134AC74E4E9DB395D6B9B7D6CFF71E3F3DC" + }, + { + "path": "crates/oa-desktop/src/workflow/transition_tests.rs", + "bytes": 8553, + "sha256": "589B1B2DBADCA74A52C2CFCFD4FE620BD4790ABD113AC02182E91616C52D793C" + }, + { + "path": "crates/oa-server/Cargo.toml", + "bytes": 778, + "sha256": "527FF5E15010A8B560A6659AA1AE4BF0C8DF0D9E019F03C47529EC6A750C0982" + }, + { + "path": "crates/oa-server/build.rs", + "bytes": 45, + "sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9" + }, + { + "path": "crates/oa-server/src/error.rs", + "bytes": 5610, + "sha256": "93817F1502386E4662C96EE1C7D1C4AFC4E33BDA6FD64E46C86C8E1AFD5A9368" + }, + { + "path": "crates/oa-server/src/lib.rs", + "bytes": 7743, + "sha256": "20BEF350350ED6339588B23AF684ABF745F97916C7EDF3853EE042ABE687E664" + }, + { + "path": "crates/oa-server/src/main.rs", + "bytes": 4163, + "sha256": "D59A6A2B5C7034BD2E00A516D2D49ABB16391BF0316C1C357DDAFB9C82D2D1E3" + }, + { + "path": "crates/oa-server/src/preparation_http_tests.rs", + "bytes": 5523, + "sha256": "454B90D0055D35644C8D990CD7576DDF675A9B271CA07FE9B0EFCE25D8DDBC07" + }, + { + "path": "crates/oa-server/src/routes.rs", + "bytes": 46053, + "sha256": "3F9A0C22043DBF28F7A8E587D2FC3ABF869F76F4D23A46FE0AEB9C4392454A4B" + }, + { + "path": "crates/oa-server/src/store.rs", + "bytes": 25524, + "sha256": "37DCFB845B7993D5BCED2BCF25FEDFCD2A9C20A748F9315AFE6CCA1F41F89AE5" + }, + { + "path": "crates/oa-server/src/store_preparation_gate.rs", + "bytes": 4440, + "sha256": "427C390DA3C0281BA1FE115A93EA90800E3B45FCE6BCB9185E71982DDD885FEC" + }, + { + "path": "crates/oa-server/tests/http.rs", + "bytes": 71892, + "sha256": "50DAA6A90E96AD6FE3C6A27DF2EAEBE486270599C203892D09769C547A194975" + }, + { + "path": "crates/oa-server/tests/http/communication_limits.rs", + "bytes": 5790, + "sha256": "BA5474DD8E494A0176FFFB4548C8E3DEB2081B15DE33BFCE82394C764E0A2C16" + }, + { + "path": "crates/oa-server/tests/http/data_contracts.rs", + "bytes": 25949, + "sha256": "8CD456DB83F301BACA380ACF025A5A99DB4EDF7D8839537DFBD02C4307E10E6B" + }, + { + "path": "crates/oa-server/tests/http/port_anchors.rs", + "bytes": 15086, + "sha256": "7199D0A208DDAD9E1518B6E3FB390D64DA111716D7505BA5C995DEAE19AD1258" + }, + { + "path": "crates/oa-server/tests/http/semantic_transitions.rs", + "bytes": 22832, + "sha256": "856D1C9B633FFB581E3E631F4E5A607789A02181DBF060667F208A569BFA4920" + }, + { + "path": "crates/oa-server/tests/http/source6_views.rs", + "bytes": 13023, + "sha256": "867BCCDA0E030EEEB9976B4CEEB47B21AB5044D700278022D5FB495BBB64CA07" + }, + { + "path": "crates/oa-server/tests/http/source7_contracts.rs", + "bytes": 11000, + "sha256": "0E0A467186DA7263C0754890AFF69EBD0D92D36B5224E4865BB6854B18DAB7E8" + }, + { + "path": "crates/oa-server/tests/http/source_projects.rs", + "bytes": 30907, + "sha256": "8FF580F317EC267D1CD72AE14D8638F74A77CDF99CA4C78F66F4290C2D77460A" + }, + { + "path": "crates/oa-server/tests/semantic_http_contract.py", + "bytes": 7251, + "sha256": "4737FF94F10761F94151A694414C5C62737E0391EC820BCA132830652BC4CCA2" + }, + { + "path": "crates/oa-server/tests/semantic_schema.py", + "bytes": 25111, + "sha256": "CACCA116C42FFBC097F1419C0F89921FA0DFD3FAA864D46820FF744CFE6967F9" + }, + { + "path": "crates/oa-server/tests/state_representation.rs", + "bytes": 12241, + "sha256": "6686C826F28248AE4B63EC3F2112E193D4A347724B87974483217FCFEE49CD61" + } +] diff --git a/harness/experiments/canvas-union-windows-20260911/compile-inputs-6969c82.json b/harness/experiments/canvas-union-windows-20260911/compile-inputs-6969c82.json new file mode 100644 index 00000000..1a490f61 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/compile-inputs-6969c82.json @@ -0,0 +1,1407 @@ +[ + { + "path": "Cargo.lock", + "bytes": 119973, + "sha256": "065C23C94908FE499D32ED91D55A70946ED5B08ECFDA3B483EC1E635572257BF" + }, + { + "path": "Cargo.toml", + "bytes": 400, + "sha256": "369F5B4B2B60705A94623C8C0B68F8E0B048F263280CE07AE0629AD353797D62" + }, + { + "path": "crates/oa-cli/Cargo.toml", + "bytes": 279, + "sha256": "49C567F1AF74A826683B4D96845FEB3806FD9BCB7DC2767E16B5509D4E5E1CDE" + }, + { + "path": "crates/oa-cli/src/lib.rs", + "bytes": 15498, + "sha256": "97BEBAB63B4A611A1D36AD0C20FE452CBE1985FE3A6FBD39C954603DA11410FC" + }, + { + "path": "crates/oa-cli/src/main.rs", + "bytes": 2984, + "sha256": "DE964F9B6C59C0F31C080AFBD3B31D442B07B1B44F6A187A248D3409C45D35FB" + }, + { + "path": "crates/oa-cli/tests/source_cli.rs", + "bytes": 16600, + "sha256": "A3A1B856C7A9FD3D80BC8074150A24FBB0EF0E9DDF736C7CEA036725FC3DC2B8" + }, + { + "path": "crates/oa-core/Cargo.toml", + "bytes": 408, + "sha256": "6B5C062474EBB62D5082E07DABED99EE85E4A833EDF26690FB4F887028D3416F" + }, + { + "path": "crates/oa-core/build.rs", + "bytes": 45, + "sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9" + }, + { + "path": "crates/oa-core/examples/generate_demo.rs", + "bytes": 795, + "sha256": "E26F432C95331AECA50985DBB3AD2C558AFFD43023B585811F0F7BFAA59F6661" + }, + { + "path": "crates/oa-core/examples/generate_release1.rs", + "bytes": 1798, + "sha256": "1D5F11165D74893E4BC18DBF3A2F2111F4026BD89FD06EE7F0BAE69E45C4D4B5" + }, + { + "path": "crates/oa-core/examples/release_dense_views.rs", + "bytes": 14664, + "sha256": "A83158488EF9B7394F136C52B1496719BE935C4E5BDFE457CAB5C9894F5F9412" + }, + { + "path": "crates/oa-core/examples/routing_workshop.rs", + "bytes": 17248, + "sha256": "571307B18B6D4FA04700C5A4D4B0C2154C419E93A21292B2028E7B6F46A2FAA7" + }, + { + "path": "crates/oa-core/src/analysis_numeric.rs", + "bytes": 3735, + "sha256": "6C88EEF3119162AEB6C07B9E75812C3AF9FBFE75554B0503AD00F103CB478408" + }, + { + "path": "crates/oa-core/src/analysis_numeric/dimension.rs", + "bytes": 3045, + "sha256": "D4AD8E5307A1620E31791D724A0E31FDBA860D00DC83E3E1F2209D9C5E22A340" + }, + { + "path": "crates/oa-core/src/analysis_numeric/exact.rs", + "bytes": 17338, + "sha256": "4EB8CB0F66B10E5DA204899AF82EE26ADFEC74BAB47B1A6C9842B5488EE04C4D" + }, + { + "path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json", + "bytes": 6085, + "sha256": "3B959DEC5E100A6F03817F5314BEF497347BA579EBD235476516D4BD83399181" + }, + { + "path": "crates/oa-core/src/analysis_numeric/meter.rs", + "bytes": 7129, + "sha256": "1F7174BEB992935B4C399ACDEF91114AF73FAF041070FEAB968455AC827E8EE4" + }, + { + "path": "crates/oa-core/src/analysis_numeric/quantity.rs", + "bytes": 12257, + "sha256": "EAD5142D6EEEA8F99399D57E02D8F7D819980923DE76387CD454694D6A747AEE" + }, + { + "path": "crates/oa-core/src/analysis_numeric/tests.rs", + "bytes": 27486, + "sha256": "323CE82FC057D36F44463569FD8EA324C7D700FDDFBF32BCE15E04BD2A66970B" + }, + { + "path": "crates/oa-core/src/data.rs", + "bytes": 42696, + "sha256": "7F9C7C7DD8353CB5A38597B92CDE6D3E890203D392473187EB387D41FF37414A" + }, + { + "path": "crates/oa-core/src/demo.rs", + "bytes": 20657, + "sha256": "AD62123F96C20FF023CAFC1A9849B0FA06BC62EDD1FB9701799E4599B458600C" + }, + { + "path": "crates/oa-core/src/language.rs", + "bytes": 99938, + "sha256": "638F08A42E9CF4996B5D07F69B4D6F517A518369F3604D25BE1006E40E080765" + }, + { + "path": "crates/oa-core/src/language/semantic_source.rs", + "bytes": 7326, + "sha256": "584B5BB59D87A7B8D44D5DD2D124C3A1323B21492B138A17BC9526C0087C9E29" + }, + { + "path": "crates/oa-core/src/language_nesting_tests.rs", + "bytes": 20639, + "sha256": "BD061611E8CB6F28AB203C73E544AC98BF275EB464CD314AD5FBD25F773DF33B" + }, + { + "path": "crates/oa-core/src/lib.rs", + "bytes": 549, + "sha256": "EA1D9BF5ACBA8381BA204DB12FF5B5FE1F320417D17AF517E46B90FD6BCB241C" + }, + { + "path": "crates/oa-core/src/migration_v2/README.md", + "bytes": 1515, + "sha256": "FB61D3FAA33D7BDFBA73415415A0882C0FBD3E0B45DF42F972A19AEC6DA927B9" + }, + { + "path": "crates/oa-core/src/migration_v2/adapter.rs", + "bytes": 9490, + "sha256": "66741B4C8354A23B8A932ECC5BF1BC3BB6E74279558757B8C35BC2157A7E51B5" + }, + { + "path": "crates/oa-core/src/migration_v2/mod.rs", + "bytes": 1158, + "sha256": "857D8BB13B6814DABA6780CA770C16352943C01CB130245D055F11B3B957B3E9" + }, + { + "path": "crates/oa-core/src/migration_v2/model.rs", + "bytes": 52059, + "sha256": "8AC63EB59B2E57BDECFF2477EE0634096090924D3DD1851E1C454880A889DACB" + }, + { + "path": "crates/oa-core/src/migration_v2/references.rs", + "bytes": 13369, + "sha256": "33A6819ABC83139FDAEBEEBD1B553238A328637106CCE7DD5A535F1791D43E20" + }, + { + "path": "crates/oa-core/src/migration_v2/source.rs", + "bytes": 21946, + "sha256": "74FEE9B24E7C912182C998AB9D9AEEC125E2009C3214959229E844C67AFE2BF6" + }, + { + "path": "crates/oa-core/src/migration_v2/state_validation.rs", + "bytes": 15769, + "sha256": "E29EF071BAB0A68460DDF55C46131C6F66C022D52C3C602C37C013C3649DA1B0" + }, + { + "path": "crates/oa-core/src/migration_v2/transitions.rs", + "bytes": 704, + "sha256": "F74174C532BA56B57C80B719A11FB4ADEA0A78CCC2828FFF72FE2B9EDA5A10B1" + }, + { + "path": "crates/oa-core/src/migration_v2/validation.rs", + "bytes": 98736, + "sha256": "6D5E4A4E2805AE5045BF789A0485E14A61C63346587BACE2B4A91173A688F780" + }, + { + "path": "crates/oa-core/src/migration_v3/README.md", + "bytes": 2128, + "sha256": "3755A63F840D7F0EADF68AFA7A9D587877A9468FF982D3610539E8A0BB067A82" + }, + { + "path": "crates/oa-core/src/migration_v3/mod.rs", + "bytes": 23023, + "sha256": "F4EDB6A0B50ECFE8670973776D703047B064420CB1F05913129C00FC656B3722" + }, + { + "path": "crates/oa-core/src/migration_v4.rs", + "bytes": 4020, + "sha256": "50C652964F0F309E7F3F54B6EF12C40D42FCECC0490F005995C713DF1179AF70" + }, + { + "path": "crates/oa-core/src/model.rs", + "bytes": 66352, + "sha256": "4585C895C64C4A183941C92CF047A9E52FF0CBE41971E16E78D229EA9DC101FB" + }, + { + "path": "crates/oa-core/src/operations.rs", + "bytes": 11405, + "sha256": "A5FFBBF54CD440316A106D1188BFB699B57DAE59F466EDCB33618C333CD19F41" + }, + { + "path": "crates/oa-core/src/persistence.rs", + "bytes": 31774, + "sha256": "1C01474F906A63E552C0D590B272C95821C00A100B3E79E58E67DBBEAC341A14" + }, + { + "path": "crates/oa-core/src/ports.rs", + "bytes": 45413, + "sha256": "5CAC12EEA70A54EBEBBD6BE77B63CE92E3195325C4BC7D27705C518686CBAC80" + }, + { + "path": "crates/oa-core/src/projection.rs", + "bytes": 21374, + "sha256": "69ED12BACE2CC5024E151752A174AB14034AC2D05EB741FCF13E9A27521C059B" + }, + { + "path": "crates/oa-core/src/projection/port_layout.rs", + "bytes": 13474, + "sha256": "A08D17D10A58BB8B206DD001B19B91D5AA3A9F743B298A4DA57D503223C20079" + }, + { + "path": "crates/oa-core/src/projection/state.rs", + "bytes": 5233, + "sha256": "73A607BAC713EC7FF49A509FEFB1A53C7686DCA93EE241FFC835564D743AFB28" + }, + { + "path": "crates/oa-core/src/references.rs", + "bytes": 21957, + "sha256": "93553C234F32DA35F1F41A7B83AD81C471C9803EFA78CA222CF7DE3C2F86C0BB" + }, + { + "path": "crates/oa-core/src/routing.rs", + "bytes": 50309, + "sha256": "C5EBB350BA76F731F5C48D968BFBBA4B6EF68EC046EB5893D116C4637EF15080" + }, + { + "path": "crates/oa-core/src/semantic.rs", + "bytes": 540, + "sha256": "F9895DD5B8FB27D10D4CFBB41C3542EF2018365A589B8D5778E5674AE5A1F239" + }, + { + "path": "crates/oa-core/src/semantic/catalog.rs", + "bytes": 49407, + "sha256": "1FF74FCCA18084D178681D7FF38EE1CCAC632C27C4DE0595BA1CEAB8067AB3A4" + }, + { + "path": "crates/oa-core/src/semantic/historical.rs", + "bytes": 15271, + "sha256": "EAF3284BF234312698514CDB636BC8348E1B5D746CB16B18DF777E77D312ABFB" + }, + { + "path": "crates/oa-core/src/semantic/linked.rs", + "bytes": 6035, + "sha256": "6F335B14E809AECFC35DEFCAF77CB821B080D1506DBC46297912B25AF98C07A2" + }, + { + "path": "crates/oa-core/src/semantic/mutate.rs", + "bytes": 22683, + "sha256": "DC2B4C4A6EB156FE468EAA8C85FD37A505C0CD8EA1F226CB69CD01689AFD3D9D" + }, + { + "path": "crates/oa-core/src/semantic/review.rs", + "bytes": 51116, + "sha256": "0C157D2C327E72E89CCA3B423E71EE73798105C5BB7271439C1A101642C91E3E" + }, + { + "path": "crates/oa-core/src/semantic/types.rs", + "bytes": 10405, + "sha256": "6922FE35089F12FAD43AB0CE1C6886D01A3855F90B8E2543E99802AC5E6F1BBB" + }, + { + "path": "crates/oa-core/src/session.rs", + "bytes": 35237, + "sha256": "1EB77B329B2986FC6F5DC0171E77FD860375CDFD8C769A1367B445D6C49EBE00" + }, + { + "path": "crates/oa-core/src/source_files.rs", + "bytes": 74803, + "sha256": "669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683" + }, + { + "path": "crates/oa-core/src/source_project.rs", + "bytes": 61843, + "sha256": "1394370BCC5908873B830C2C11312E6E1611AF41F46C4F43CA0A6BCB713BB466" + }, + { + "path": "crates/oa-core/src/source_project/source_migration.rs", + "bytes": 4700, + "sha256": "ADD463AE85C420F853D324B00A221F0F874164CC3F3ED6BBB07557A9ED7A8C95" + }, + { + "path": "crates/oa-core/src/source_project/source_structure.rs", + "bytes": 20225, + "sha256": "9F1F83315856A7025FE662C0FBB13452F31613224A03663150D6225D1DDC162F" + }, + { + "path": "crates/oa-core/src/state_validation.rs", + "bytes": 15665, + "sha256": "E28D7F74385E2A78763716B34491BD73A00BFECCC085970CEB130627E3FAEE18" + }, + { + "path": "crates/oa-core/src/topology_context.rs", + "bytes": 13734, + "sha256": "16DE69B297DE95D93C73808621607833B713AF811DD082F0169186AE1E5C3E2D" + }, + { + "path": "crates/oa-core/src/topology_context/analysis.rs", + "bytes": 20036, + "sha256": "DEAD6194F0AA60CC46045C0E5EE10DBDBB689158C38634DFBBBC45D2BA88AC65" + }, + { + "path": "crates/oa-core/src/topology_context/analysis_tests.rs", + "bytes": 5258, + "sha256": "17839263410815E96FE027D4EBA3AAE0A9AB3B82B16947E2B29EBE0D09782B56" + }, + { + "path": "crates/oa-core/src/transitions.rs", + "bytes": 51998, + "sha256": "FADD1B07B89CAE4117B91ABB4F1AC9EA81FF818EF20F2175E76EE8CF21FB708D" + }, + { + "path": "crates/oa-core/src/transitions/communication.rs", + "bytes": 8634, + "sha256": "2B476630A88EDD9021B9156CA12A5402E01604095F0D4002AFB452671903A028" + }, + { + "path": "crates/oa-core/src/validation.rs", + "bytes": 112792, + "sha256": "7C1B4AE5E4EBBA13E99AF9158CCDE138DFC5E9A7AFB580E76FBA72D0B64BA74E" + }, + { + "path": "crates/oa-core/src/validation/coverage.rs", + "bytes": 6478, + "sha256": "B08E4B344ECAD055714168C83BEFF7FE29BA4EDC53EDBFFA2EAE04E5580A4D5F" + }, + { + "path": "crates/oa-core/src/view_options.rs", + "bytes": 24481, + "sha256": "421EDE0A04C593CA1DF0E6E0D06FABCFCC8113CDBCFB0ADC7F5C8EF4D1724208" + }, + { + "path": "crates/oa-core/src/view_options_tests.rs", + "bytes": 21433, + "sha256": "64AAC44DE2DF42A1012239A3DD75C473DAF0C8990B3170B7BF7E78A556080208" + }, + { + "path": "crates/oa-core/tests/communication_boundary_scenarios.rs", + "bytes": 8816, + "sha256": "E1100B8F363910A487D90C73ACC7DF543DC6D532158C3C63AD11CD3D40D8E12B" + }, + { + "path": "crates/oa-core/tests/communication_means.rs", + "bytes": 18102, + "sha256": "A25F2749180BC6F4BEB1445BFAD3351EEDF0669131A7439B633AB804439E95A4" + }, + { + "path": "crates/oa-core/tests/coverage_diagnostics.rs", + "bytes": 20527, + "sha256": "E2E9C3996DAAB47F2BD23F5D8C48B4E372AF33331152F5FC85B76F06D04FCDF4" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia", + "bytes": 29220, + "sha256": "F0DF129C1914BFC01D0B047B0CD4B520D8CF0C249EA9CED085DE400C0BE87FF3" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json", + "bytes": 43990, + "sha256": "3ED53508EB544BBD788832F7BEBEA2EC31B3D66191F24F45098CC9750EC4C005" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia", + "bytes": 2450, + "sha256": "305F19D1B79F99F8D4CF627BA2E9CA6836BB7959CC0070227815937A699EAFEA" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.json", + "bytes": 8240, + "sha256": "1B41299C53204E6232397D25A58D5BCCDBBC16E770C75E8EDB55CE36CCFE58A2" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia", + "bytes": 1222, + "sha256": "5FA53A26A5AD010C8E59EB56D9B2B52003569E66C0377FFB58840590168AD0C1" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json", + "bytes": 1734, + "sha256": "2808088AFCC165F4BD6DA5129151143411C997041C3DE61D600AB76E05D24A47" + }, + { + "path": "crates/oa-core/tests/language.rs", + "bytes": 56482, + "sha256": "77F6CB2A69976BFB5F60B243B71C07FB0EDC7505002C1E9E73FCD6DDF6D9496E" + }, + { + "path": "crates/oa-core/tests/port_anchors.rs", + "bytes": 26893, + "sha256": "EFBC0FA18733B1AAAC501F9785A4C392FC49C05C45A5778D26E9B5DD951CA6FA" + }, + { + "path": "crates/oa-core/tests/port_migration.rs", + "bytes": 15620, + "sha256": "1801C376A5435AC7EFB2A8342348B2C7FFEF930BBA65DECA4ACB66B06E0483D7" + }, + { + "path": "crates/oa-core/tests/port_source.rs", + "bytes": 4858, + "sha256": "8D2DDACA4045D330F6EDA2591561497B858AAD53769A823249D0BEA4F5916E12" + }, + { + "path": "crates/oa-core/tests/prepared_commands.rs", + "bytes": 9555, + "sha256": "52CAB1C668064777412DF0A6A3C20D74D579B33794B4B00525B9F151C06EAC9B" + }, + { + "path": "crates/oa-core/tests/process_diagnostics.rs", + "bytes": 1104, + "sha256": "1DBBC590239EF7542BE4A2BDC9543AAA64D108417B03DD312A0894E364745906" + }, + { + "path": "crates/oa-core/tests/r3_constructor_ack.rs", + "bytes": 24163, + "sha256": "E5C44676EA0AAB97F2E091DDAC2F4BC4A4A90F5877F7BBF08B61229600944DD0" + }, + { + "path": "crates/oa-core/tests/r3_migration.rs", + "bytes": 6590, + "sha256": "EBD04120F5215B3956FDC79EE78F6E38734C7D13392F1FC28AABA957912265BD" + }, + { + "path": "crates/oa-core/tests/r3_semantic.rs", + "bytes": 77473, + "sha256": "49C3763EAF2131070A7C46CC23A84F917D292D41610B6404510E5607F8890C32" + }, + { + "path": "crates/oa-core/tests/r3_source.rs", + "bytes": 15311, + "sha256": "F31F60659DF3C7EF601650D1CFF140589D8729F01A3F65A4A6A6C2272F15C408" + }, + { + "path": "crates/oa-core/tests/r6_data.rs", + "bytes": 41437, + "sha256": "5F7FFB3BC73C3C85423623FE56F8680646DC97040327BDBEAD9D47A37E80F155" + }, + { + "path": "crates/oa-core/tests/r6_migration.rs", + "bytes": 9174, + "sha256": "81519BC1512F5E069432E9A1CDAD04C10216D492DF01B4B75B02102480EBD6A1" + }, + { + "path": "crates/oa-core/tests/r6_source.rs", + "bytes": 9519, + "sha256": "88A0AA087A7678E2898FF6057538072CA92072190712F05FFF08372CC56EBED6" + }, + { + "path": "crates/oa-core/tests/release1_teaching.rs", + "bytes": 10010, + "sha256": "BE179290D871125DC97CFD2A3067C7AC89C8A538FDB0AFAFC5B825BCDFB2BEF2" + }, + { + "path": "crates/oa-core/tests/release_capabilities.rs", + "bytes": 19422, + "sha256": "B92CBA2A562E78287DDD7DC3E94557848D17FAA0C8A102601B50286B9AF48A30" + }, + { + "path": "crates/oa-core/tests/release_diagrams.rs", + "bytes": 7689, + "sha256": "CEFCC0582D91266D5DE6EE5DAEFCFE41B606AB971AB235B59034A8E1DBC24B7D" + }, + { + "path": "crates/oa-core/tests/release_language_limits.rs", + "bytes": 5204, + "sha256": "8578DA4F76BBD2D0E1BFE295AB1259E2162A3FE3D0D2BA8901915D7A40942806" + }, + { + "path": "crates/oa-core/tests/release_ports.rs", + "bytes": 32740, + "sha256": "506E62BEAB0C7EC0CECE83E9E0C1A810FA1F99285C9E1CAB0C77F06DE55A40E1" + }, + { + "path": "crates/oa-core/tests/release_routing.rs", + "bytes": 23165, + "sha256": "AE168FAABA23769B7CA1FD65F217B9DB671726E67276EB5A04E5E72B1447A462" + }, + { + "path": "crates/oa-core/tests/release_scenarios.rs", + "bytes": 41067, + "sha256": "0F5D876BADEEC653FC7B12610A77D867E9A53582E605253DEFF8D379DA21572F" + }, + { + "path": "crates/oa-core/tests/release_states.rs", + "bytes": 26873, + "sha256": "B1A26ACEE95EABA7F9920F9DA738A1E33606552EB078E6D298895DAFF7E3D811" + }, + { + "path": "crates/oa-core/tests/release_topology.rs", + "bytes": 17643, + "sha256": "C51DBE5AA5A038F9ED830DAA02AA67714472179EB7FD6DA5CE36DC6931C8B3A9" + }, + { + "path": "crates/oa-core/tests/review_regressions.rs", + "bytes": 21823, + "sha256": "F154E5C88D35E31512E2B07733A1327EA3E2FCFD1EF8EB23066A4E9573F387C1" + }, + { + "path": "crates/oa-core/tests/route_source.rs", + "bytes": 7270, + "sha256": "1A73FDBAC0D0AE0E84D8D67B6AD398E11223958F5DCA007F73757C91827DECFB" + }, + { + "path": "crates/oa-core/tests/routing_geometry.rs", + "bytes": 31890, + "sha256": "2D8EEA4A2BBA535ED0EA6DAABCCDAF87B375828C404BAA10518C7BD7FD6253D3" + }, + { + "path": "crates/oa-core/tests/routing_labels.rs", + "bytes": 5530, + "sha256": "DFF884D799C3C5C9257210248C55767E294E3CB317C605CBD0DDC9600C216EB6" + }, + { + "path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs", + "bytes": 4640, + "sha256": "D93EAF9667F4221232168BEF0FE9F069E5BE1B1F23410BDA5DD67010B96E5C8A" + }, + { + "path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs", + "bytes": 14957, + "sha256": "7F526D363BA05E7AC89FAF0FEAD0796E419C34EE204EDF44A1B46C78308B0970" + }, + { + "path": "crates/oa-core/tests/source6_view_contract.rs", + "bytes": 14307, + "sha256": "FA566DE53946D40BBBD9033EDFEED177180C4DC44AFA2F2F4667FFDC622EB3BB" + }, + { + "path": "crates/oa-core/tests/source7_contract.rs", + "bytes": 43262, + "sha256": "CAD4F1A135DDBD947BDF3C71D1B44F1E438725095EA8C4D893464BC721C2363F" + }, + { + "path": "crates/oa-core/tests/source_files.rs", + "bytes": 13168, + "sha256": "F0429A0B89A443458EA9AF7083C1E4E7411417A7E6B47E618BFBB9266DC164F9" + }, + { + "path": "crates/oa-core/tests/source_identity_updates.rs", + "bytes": 5139, + "sha256": "F52AA38B4D317E532C641AD7463472E4B40E802E60052CF0017556FFC63DA06C" + }, + { + "path": "crates/oa-core/tests/source_nested_updates.rs", + "bytes": 16892, + "sha256": "9AC3390936C0D250651EA94A1B8FAF0ADFB680F061ECECC56F2A52909B5BA855" + }, + { + "path": "crates/oa-core/tests/source_project.rs", + "bytes": 45910, + "sha256": "CEF58ADB16AA4D2527DDC0628AEC3E26F0B42B4BE70014A7E360FAFDAC13A942" + }, + { + "path": "crates/oa-core/tests/state_diagrams.rs", + "bytes": 23973, + "sha256": "6B02B387A3AE559CB3AD72DE24292A5ABCB76AAAC0ADBAB5D2AA5211B22CD2E5" + }, + { + "path": "crates/oa-core/tests/workflows.rs", + "bytes": 33426, + "sha256": "BD843DFDDBD5724EC08F657571F887496203056FE2FA5864EF50A168A613B89B" + }, + { + "path": "crates/oa-desktop/Cargo.toml", + "bytes": 842, + "sha256": "F3547B2B924D5DB6706C0A8E069014B440FF65315D82FC8ACED0360AF4B3CDC9" + }, + { + "path": "crates/oa-desktop/build.rs", + "bytes": 45, + "sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9" + }, + { + "path": "crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "AA2E4853B25BB7E185B1DE49EDA42028523C8EBF9599BFE6B7CB2A2B2A688090" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring.rs", + "bytes": 27835, + "sha256": "37C7DB33E99DD4ADCC5A51F09EE4878AF94FD32413359890A541690080885F65" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/palette.rs", + "bytes": 8887, + "sha256": "8F9937CB30B48B83759FD084A8A5990A2DAA3920280F1210ABE048560241985A" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/tests.rs", + "bytes": 14732, + "sha256": "ED864839940A1910C97537B6EE151B57AB115ECE6F60BAE602552B84F9AF4A5D" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring_tests.rs", + "bytes": 18599, + "sha256": "77EB1A0038D038B989F7AC568760E16FE23C4B8D73CD9FBE32B70817EA23CFEC" + }, + { + "path": "crates/oa-desktop/src/app/behavior_lifecycle_tests.rs", + "bytes": 13669, + "sha256": "1AF2E290AA259A6AF6B0585950CDA93FE3A562C7B8BE1E938FE1D80747B11808" + }, + { + "path": "crates/oa-desktop/src/app/checkpoints.rs", + "bytes": 29576, + "sha256": "55E5BABDCABEF767AF3BC8BE9CC3153B3F5CC0695701B890820EC0225643852E" + }, + { + "path": "crates/oa-desktop/src/app/diagram_selection_tests.rs", + "bytes": 5582, + "sha256": "4FA507ADBC23DB3A6EB202D809C695EC0EBD6181356930C4FA7528A9A0C7BE57" + }, + { + "path": "crates/oa-desktop/src/app/hosted.rs", + "bytes": 25059, + "sha256": "2F7094C078E1D7CBCCD6C0D310AFA6C400A7E031F12918BBFCE65BA62754E316" + }, + { + "path": "crates/oa-desktop/src/app/hosted/tests.rs", + "bytes": 30344, + "sha256": "FA118F64D04B400CCC01E4E67ADAF3541975288882AFA265A9E7F092CDBDA232" + }, + { + "path": "crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53D4A422D3066E07CCCB9E8EF2AD08EA95E451B9B7CBEF29A27E6634E035ED97" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38E033E23E8135032F70117A376A5FE7BC68127C466559A35D7EC200932E3F77" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests.rs", + "bytes": 20067, + "sha256": "B96DDC6BFF7F8FDE97720E18A92070F891C1340348D2E19C7C9C5A7BDA650CE8" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests/mounted.rs", + "bytes": 32805, + "sha256": "14A4914A1C86E50C395672DAB8FE80D7AF679A79C9082890B509FAD7B7C543D9" + }, + { + "path": "crates/oa-desktop/src/app/review_navigation_tests.rs", + "bytes": 7049, + "sha256": "872FA91479DAD5436E139FFCAECE270323E9F5DAEEB7A4D80C2C2534F48BC21B" + }, + { + "path": "crates/oa-desktop/src/app/save_as_tests.rs", + "bytes": 4113, + "sha256": "F8B915793F1A36FD152569C4781A4BDC17F4AF404A8CEB373330053413087A1C" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram.rs", + "bytes": 9979, + "sha256": "F7FD50CC9A86ADB794713958348AF63AFC7C698F74965E13188B7B8A9E735C55" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram_tests.rs", + "bytes": 39957, + "sha256": "612E414B1A2CAB12CBFD8FCE23745559FF27577700694D53AF67AD1F2883EE2C" + }, + { + "path": "crates/oa-desktop/src/app/transition_context_tests.rs", + "bytes": 11508, + "sha256": "47002FB025F3A2F8C53871F9509BFE8611A61DB6FB0E1B3125EB6E875425CB1D" + }, + { + "path": "crates/oa-desktop/src/app/view_copy_tests.rs", + "bytes": 7846, + "sha256": "296821C51DCDE568E7D717039AA26BFC1B56A9074AA16051007BB351B33A4D44" + }, + { + "path": "crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5E7CECB54BFCA64E4462F7881F460900ABD925BA2CFE9451CE2B0C78AF4BC850" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7744, + "sha256": "096D667C4C8EE3732D52668A9562C665F2D9A192ABDA4E5487FCAD7621C8DBFD" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7860, + "sha256": "851AF1E9B155D6D0D079D9164C5616FADC33E961391A6604A43EABF5158957B3" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "CC58CB3838F546150D22FE4DD83A3FFD52504C279FEBAEB8D0B4A6A3B8A73950" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 26992, + "sha256": "A933254D12C3B05E3FFAE84EA71C578569CC9A22B86E49644440893AB3EE2BA3" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "AF1C7C1E802D1207EE6C7BD66B703811A250D7B56511512AF6C0945C776E68A7" + }, + { + "path": "crates/oa-desktop/src/behavior/contextual_source_tests.rs", + "bytes": 49386, + "sha256": "5F05318A6533503B9D6FEBF1EC4DD7256F192394BC1B952A7904C8175BD8DDAE" + }, + { + "path": "crates/oa-desktop/src/behavior/creation.rs", + "bytes": 28978, + "sha256": "33AE9AAF4BA90DE9432870CC8E99708EF39E3638AE8283A9253C333785C91299" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/app_tests.rs", + "bytes": 13181, + "sha256": "E9C5BF2B07357B7A2F679AE0D8F6904C1C1461AAEB94FA0DB478B54D4641AE3E" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/tests.rs", + "bytes": 22793, + "sha256": "A0D1DAF9FB3920F7E6181A9802D84FBE056CBF0043B9E41DBA5FDDA687D9818A" + }, + { + "path": "crates/oa-desktop/src/behavior/form_session.rs", + "bytes": 3941, + "sha256": "F5027FB3A5A5599A182E7D1A10DBA56DD66EB1F23A4BA4C4A1744C9A6A5B6E61" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "FB1DA03E8DF05A59253F7FDF34391E30ABD8AEA427B7D0A12B86B80FF3BF9ECA" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/focus.rs", + "bytes": 9728, + "sha256": "73A9EA10AD7312086081B8688396A21AB4251C9FAECC2DF6D984B702F3963533" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284F5A7DA09BDC77D70C6788A091D57927150C796FFC10D786793CFF7DA4B7C6" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950CF4347A3EDEA615B1AC71D9A681C32B446B5109FCFD0F03F3AE2B147360BF" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle.rs", + "bytes": 9324, + "sha256": "758D244C2E8DE61F515D540BAA4D967275F49720E0253C6C9159EB9ACD71C859" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle/tests.rs", + "bytes": 15430, + "sha256": "01B576993B329810CD78A859B45AC52C66F5A14549065661DD1308196E0B724D" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_operation_tests.rs", + "bytes": 10087, + "sha256": "89FB71C690E07EAC314F7A5E05862D5C1E2F9C21039673BF22C4AC894D21C977" + }, + { + "path": "crates/oa-desktop/src/behavior/source_draft.rs", + "bytes": 18031, + "sha256": "08190823090BD8B0305809099ABADCC0DE431175F7044F1D5ABDCEDA52612EE1" + }, + { + "path": "crates/oa-desktop/src/behavior/state_canvas.rs", + "bytes": 93561, + "sha256": "2E2AD57B86F4CC1CC0C4C4C230571EF19BCC2F052001A8BDCC44925E5103077A" + }, + { + "path": "crates/oa-desktop/src/behavior/state_edit.rs", + "bytes": 35736, + "sha256": "CB2289E727163626152FC2F11195C6EE824851CB9442C57496DA97FE7AEBFB7C" + }, + { + "path": "crates/oa-desktop/src/behavior/state_menu.rs", + "bytes": 7447, + "sha256": "CC87C5CAA177E114E4D9A7F7367E37EC2B0D1E1C16B0B99709B05EA0E59CF894" + }, + { + "path": "crates/oa-desktop/src/behavior/state_view.rs", + "bytes": 6988, + "sha256": "1EB892A9139ECB0671784C879EE5114F27E598E5B3589F5CDAD0ABFFD5909F24" + }, + { + "path": "crates/oa-desktop/src/behavior/states.rs", + "bytes": 26986, + "sha256": "8986ED9492960E20F1254E9DFE3FF91D79CE9FEF66F3BDA738F9933278043954" + }, + { + "path": "crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723E51A7DA4EB224D51C1659EBBFA4DC8B11557BC8BF70F6E3E1649BAD24449" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5D0AC5B3E258FC70987A00EDE1E603681D99746596F2FCC46AD2B3667D65203A" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual_routing.rs", + "bytes": 16731, + "sha256": "D914B0E82AA8D253FE64BE2EF74443D1FD2E6CEC26D1CF30620A57646E1D426C" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309B8978EC2586FF4614236C2F624AF6D934D117363E917B8C1CF9F2B1295EB" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733F8BADAFB9BA87E3F861F03F78809450EEE72CC118341EB03B24904790144A" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "39D9C28201EC1B044EAE848B09AB276FF7FCF9F69FE6B5E39A6863F56A0111CE" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "B2D502C11426BFFB8763515F2A56AA4C9F8E203BB36D65EB34F81C123FC406A6" + }, + { + "path": "crates/oa-desktop/src/connection_edit.rs", + "bytes": 5301, + "sha256": "020581E077F82B43C936B623CC51733B02B88428DF4E5F25DF74B9705819FA37" + }, + { + "path": "crates/oa-desktop/src/connection_tool.rs", + "bytes": 27023, + "sha256": "649DA93677D52DAE495B0E19BC32F3122AB05A6D84DE8402578FF5CE3F6EE1D3" + }, + { + "path": "crates/oa-desktop/src/context_menu.rs", + "bytes": 52955, + "sha256": "629354B34DC232763700251F2695B4D4E53D6251C0B1432ECC0DFE424F7DFEAC" + }, + { + "path": "crates/oa-desktop/src/data_values.rs", + "bytes": 23877, + "sha256": "E1DEB1CAD551F0A9FDEF9653AAD257CFB36E6D9C0F93B6245061EC3FAB8FD365" + }, + { + "path": "crates/oa-desktop/src/data_view.rs", + "bytes": 38871, + "sha256": "158B852BE5D3DB450AA5CDA8B68A35BD96BDDCE1D8D053A7A23582095C06362D" + }, + { + "path": "crates/oa-desktop/src/diagram.rs", + "bytes": 105256, + "sha256": "4580B170E264D14E5CFA874709F6D2E076A05155BFBE1809336A9D05F75E98EF" + }, + { + "path": "crates/oa-desktop/src/diagram/lifecycle_tests.rs", + "bytes": 9316, + "sha256": "D0E72CC0574657D5A31C3CE307B58401D0ED77EEDCCD1AA02377F55AB0BC03F0" + }, + { + "path": "crates/oa-desktop/src/diagram/pointer_tests.rs", + "bytes": 11833, + "sha256": "10CA5049034159205AC8F22805354F308B0BA0596F331568C26CD1393BC8CEDC" + }, + { + "path": "crates/oa-desktop/src/diagram/port_hit_tests.rs", + "bytes": 5624, + "sha256": "D2D0647629BF725B4DDDFC211F5A27D495ED093A79AF87CDAE6C196F09A97B93" + }, + { + "path": "crates/oa-desktop/src/diagram/port_labels.rs", + "bytes": 13408, + "sha256": "28782983B82E4FCA5DD4410AC8BD562359943B7010C7B24ECC42F0C7AD7FB63E" + }, + { + "path": "crates/oa-desktop/src/diagram/view_options_tests.rs", + "bytes": 2815, + "sha256": "CF0C98A7715558BAF17B59F67A80556F98AD30262B4434635E617C6601775433" + }, + { + "path": "crates/oa-desktop/src/document.rs", + "bytes": 31080, + "sha256": "2ECC2CD496D4A83FD7A32B9DC68E49F0F1CB31A06263DFD6939B4764DA5B73E2" + }, + { + "path": "crates/oa-desktop/src/document_save_as_tests.rs", + "bytes": 5781, + "sha256": "83DC243A25F4E97674FF0D86C794A490CC923E98F1F9FB05BB038F622F11E20B" + }, + { + "path": "crates/oa-desktop/src/explorer.rs", + "bytes": 14764, + "sha256": "FA01C59219564BA9AE5553ABC2B6F94E9849F9148D5812EF99B40318FE5DCF9A" + }, + { + "path": "crates/oa-desktop/src/explorer_view.rs", + "bytes": 13773, + "sha256": "A720D7484BCC3A24729332A00E005D1CC6F5F945A9CC3965134E0024DFFB581F" + }, + { + "path": "crates/oa-desktop/src/forms.rs", + "bytes": 105544, + "sha256": "EA124F479FCD2D3978AC3CB8EB81FEC4A7AAE5E4F4414C4BDD54849E954DEDF9" + }, + { + "path": "crates/oa-desktop/src/forms/context_fields.rs", + "bytes": 10831, + "sha256": "EAACD474F6500DD344D5A8B63CCB1D1DD86FE2AEA78AB708AFA8A80D62CE69BF" + }, + { + "path": "crates/oa-desktop/src/forms/data_fields.rs", + "bytes": 49285, + "sha256": "A540A0D451BFF56A96B1D28643B2E8C0649F3846350C25C5B154729C0538E77C" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields.rs", + "bytes": 27414, + "sha256": "E7100474CDE18BDFE95562BCB3C055F29B4B15AA64788A578194464AEBCD046E" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields/tests.rs", + "bytes": 33950, + "sha256": "16D3C308138C063F45986D53C621AD83C7DC2CE853EDD31B7204E2939A382DF9" + }, + { + "path": "crates/oa-desktop/src/forms/port_fields.rs", + "bytes": 30554, + "sha256": "3FE684D651D5BE6203FD9DB3979C772913E1AF5AC1514E67BE386FFD88DC8127" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser.rs", + "bytes": 12130, + "sha256": "B60AF363BE333AF048859E51C28E8C989CB5C95D696CAD16C8EEC15F78D57A12" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/adapter.rs", + "bytes": 10447, + "sha256": "87AB4BC3BF27FD4B5A458791CF62C7BA8E44E9EBF846D8B851B8B6FFB454CD64" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/tests.rs", + "bytes": 27835, + "sha256": "8E98F8A3FCB814D4A201CC85B3DB87A689FD3D2CE487F8A2FE312FC296F93377" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger.rs", + "bytes": 13336, + "sha256": "34D0ACECFE6BED193970AA473CC3313984971264B86D7B362A6B84C23D013C0A" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger/focus.rs", + "bytes": 9923, + "sha256": "F1E79F4B03B2F9FD1CDC335054D4AE177DC9E759B6EA3D334B2BCCA39B57191B" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/widget.rs", + "bytes": 33804, + "sha256": "D66CC17AF7A48BB8598BE683F084BB08FA7AF7304F9636F0A1BEA94C5CF6C727" + }, + { + "path": "crates/oa-desktop/src/forms/view_fields.rs", + "bytes": 24987, + "sha256": "5184D438F0DFD4E29F284FF048E19DA73E71BE1F24A6E9F123C7E922AECB85FA" + }, + { + "path": "crates/oa-desktop/src/guide.rs", + "bytes": 113998, + "sha256": "EE57F8847FF1692AC566D19A66CAA7BB77AD49B5F4E30C562B93117C59BC2671" + }, + { + "path": "crates/oa-desktop/src/highlight.rs", + "bytes": 2751, + "sha256": "DA7C254FFF7052E5FD9EC7469F473B91A819406028D7C304F6E22C4169561600" + }, + { + "path": "crates/oa-desktop/src/home.rs", + "bytes": 17587, + "sha256": "F03C1071334894F586A5A7C86026602A694DB3C2FAF902822E1DBE5156A859C2" + }, + { + "path": "crates/oa-desktop/src/home/tests.rs", + "bytes": 8167, + "sha256": "3B03CCFC97DFB75954E1E0ED568BB87A632295C07B96CEDC05ED381F87B378F3" + }, + { + "path": "crates/oa-desktop/src/hosted/client.rs", + "bytes": 21912, + "sha256": "54B24B4B50F8D87D7C92946EA5A2CFF4C98760E70555B3CE3D90BE27115E95D4" + }, + { + "path": "crates/oa-desktop/src/hosted/client_tests.rs", + "bytes": 23009, + "sha256": "3EF70907CA75E098D631C41B90BADD4CFF1E1EB1B3C78E15A9F811C13BA8FFBE" + }, + { + "path": "crates/oa-desktop/src/hosted/mod.rs", + "bytes": 3744, + "sha256": "1B75E11C80FCB38FD20BB3487E3F88729FDAC73E471C5AE64DA28C249F77C63B" + }, + { + "path": "crates/oa-desktop/src/hosted/view.rs", + "bytes": 9281, + "sha256": "FC0C7B7CE3A5F05C49713DF6661067751F81E810AF11C614CA265EA9490E6463" + }, + { + "path": "crates/oa-desktop/src/main.rs", + "bytes": 1511, + "sha256": "CB0A919FD0DF0924591283DA5B55D64D525D8461640441509350BE840235E51A" + }, + { + "path": "crates/oa-desktop/src/model_menu.rs", + "bytes": 34680, + "sha256": "12994A5433A91F8295A59D4BF40F35F1EDB8D8DBB497A11EAE8760299627F1D4" + }, + { + "path": "crates/oa-desktop/src/navigation.rs", + "bytes": 8839, + "sha256": "71355A12E1B4791106426D83FE5EBF0B29BCE8E1DC594F0655AB85695469669E" + }, + { + "path": "crates/oa-desktop/src/port_edit.rs", + "bytes": 7809, + "sha256": "70B99F577C4FDEDA8780C8DC1EE7B1639FD77AACA06658F06D725139FB3EFB6D" + }, + { + "path": "crates/oa-desktop/src/port_preview.rs", + "bytes": 35855, + "sha256": "069487C4ECE737D52E8AE4A84DCFB132236ACA6799E841C91C8E7244320CF871" + }, + { + "path": "crates/oa-desktop/src/port_workflow.rs", + "bytes": 40582, + "sha256": "CCC15F97C3272145A67BB5857B4FAEAF7437561BD61912FA27AB26E8824146A0" + }, + { + "path": "crates/oa-desktop/src/recovery.rs", + "bytes": 21041, + "sha256": "57E22E5FBB0E994E64AE03E69450271FE9F04A951DB1670C2A018ABE99DB4489" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/README.md", + "bytes": 1587, + "sha256": "A90002DF1E087D2DAC7BE26926864243568A0DE7C4103E4927435B34178E8016" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/profiles.json", + "bytes": 473, + "sha256": "2C12AE96B50E6DA5D60CE2B7FA00204BE71DFBE2FD51311FC337A0DE6A4332FF" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-bundle.json", + "bytes": 80974, + "sha256": "12715995E6F7F35414AEA53B3AD720B9B31873FFDEAF4444231DB1A5AC3C463E" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-model.json", + "bytes": 86191, + "sha256": "5535A706DF9EC21715E8B6A5AA6DBD79636AE461450AEE61305559016167612B" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-bundle.json", + "bytes": 85286, + "sha256": "EA88890A33F47D67DFB06D6DBBCC179058CF28DAD76AE7D615C00AAF30DBBBFE" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-model.json", + "bytes": 90629, + "sha256": "5BB8DB0B6394276666323483F977AC3586F88554C81A148C6D508FCA0EF4576F" + }, + { + "path": "crates/oa-desktop/src/recovery/source5.rs", + "bytes": 6956, + "sha256": "0F0A763F0C98C2C908D4C2B95D8A4C042B6A255EE1CC16260E5404669691F642" + }, + { + "path": "crates/oa-desktop/src/recovery/source5_tests.rs", + "bytes": 12146, + "sha256": "CFD17A742F1F27B523FCE7CD9309F0A07A30413395F6C58659FB91D02834A7A3" + }, + { + "path": "crates/oa-desktop/src/recovery/source7_tests.rs", + "bytes": 18213, + "sha256": "7EE61DE66C75BF949489BA51DDC9677E2B4EFC68801CF27909C9B894BFDC9288" + }, + { + "path": "crates/oa-desktop/src/route_edit.rs", + "bytes": 2941, + "sha256": "457AE59C2761A43A8620B4D336A7CF0F54B965997110ECAF968756A88B6B0374" + }, + { + "path": "crates/oa-desktop/src/semantic_review.rs", + "bytes": 3432, + "sha256": "8DD1E93783D63A663C7E56C28775A5C389CFD7A6B562013D6A95C891AFA21ED8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts.rs", + "bytes": 24201, + "sha256": "A79236A8150597FF808597E59CF4F1E9E61C74C4901D9C8CF42D3427BB52A9A8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs", + "bytes": 11034, + "sha256": "3F255D04E4BFFA5A467B75E3CBF6F7149AA3269A02F2A89FBAA1DB0E5DF9AE62" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_tests.rs", + "bytes": 20798, + "sha256": "04D0DD2AC453705185663F705282AA4A3A31433466A5BE305764248DA1A03C32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_view.rs", + "bytes": 6447, + "sha256": "2100FF73565765D8293C84F6CE2D8E566610C1524C50E32CE3C37DC33B6686E4" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments.rs", + "bytes": 18160, + "sha256": "DE0EC0C740895B160FF5B847332C087C3F1A48931583464069470CC61315ED59" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_tests.rs", + "bytes": 29389, + "sha256": "8B26BE0324E0F33B1E7CF7AF340350DF8ED98963002D6DA8589EE0E6F6900BEA" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_view.rs", + "bytes": 3032, + "sha256": "932F231554D77C7CB119D33BB06B9879DB5E12127AED187E67013E77B6458E78" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch.rs", + "bytes": 34485, + "sha256": "A462EB627AE19348072775CF8E60B17D62F6B682B74CC413906D493FBA1F0B3C" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch_tests.rs", + "bytes": 27546, + "sha256": "B67C4D7DCABAC710CF3D30730A8DB323180A558A415E2D117C868CDFA4804619" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose.rs", + "bytes": 27155, + "sha256": "C6ADCC9882DDE65E9F04F6EDA7F0C855C5CBB285CF421123029169112793CE9B" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs", + "bytes": 9809, + "sha256": "440DAF8D43278EF981C620D1818D0DAAED8FB83193B8E8FF67B2E42FBF882938" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructor_tests.rs", + "bytes": 54132, + "sha256": "85149521AD7DF62918F6F2FAB6DA2B3346C342AB4BCE7100DE3F6206C1E81096" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructors.rs", + "bytes": 12258, + "sha256": "0982163439B06A42D9AF1643530364109282E63F05B323A35A76D8241F3CAA32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/history.rs", + "bytes": 3211, + "sha256": "DF038C2464EAB8093A983F3104F0D04CA8F602010A50E1D3EEB8E53F2309E201" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed.rs", + "bytes": 14175, + "sha256": "BDD6C0FDC7108056BC5D7433254503449EED2209813680E717EAD24B9D0CE834" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows.rs", + "bytes": 13222, + "sha256": "4B7BF25806471AD22D89253B9AF8000F5ACFF53B4CC0AA1C1A83138E7B270ECA" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs", + "bytes": 30804, + "sha256": "1A3B09E7ED100411CDF8043F9A78DEFC664A873EE0201117709185C201A5C57F" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_tests.rs", + "bytes": 31163, + "sha256": "9DC9B989E5A9D408427CA1BAF86B2454F9DD3BAF40A551C70CA2359DC18A1227" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind.rs", + "bytes": 789, + "sha256": "4ED19952EA511D1B3226564A138E3322F994371D6B49794C14BC3689D1393E7F" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind_tests.rs", + "bytes": 23424, + "sha256": "5FCF9930E949AEBEB12EAB0327264F32A2388D62DBF098BD2365711E22623C52" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered.rs", + "bytes": 9705, + "sha256": "6A1ED894D1B9D0110BC784A75C49444F1D14F1F74620D701F22203B2D8D0A813" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered_tests.rs", + "bytes": 22383, + "sha256": "5C8EA3A69E3DA3B94D13F50F12AA7DC53246CCBA25FFCACE9552A0EB76B0BF36" + }, + { + "path": "crates/oa-desktop/src/semantic_review/recovery.rs", + "bytes": 28769, + "sha256": "88E69044D90C481593BAB7CC125F0401C05202FCCDDE07698CEC9483947FE8BE" + }, + { + "path": "crates/oa-desktop/src/semantic_review/row.rs", + "bytes": 38159, + "sha256": "B08FDC503C6C4D2FF99ADAC7E18C9BAA861BF86155E00B55D1E492430980D0F2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/scalar.rs", + "bytes": 15153, + "sha256": "9AD202174934DD9A63FBF9CBA43135AEAD571DB60846C8B7B1D3019A11DF26C2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/source7_tests.rs", + "bytes": 11665, + "sha256": "5E8221D77B068FA30C1571988448CB8829711735881D810F3D333CBC95D34A9F" + }, + { + "path": "crates/oa-desktop/src/semantic_review/tests.rs", + "bytes": 32480, + "sha256": "077EC6501DFAAB0B05EA3987943B204F08352952296D313555051D998CCD232E" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view.rs", + "bytes": 57626, + "sha256": "7CC7DD213AB01DADAD5DEBAEE868C30A1099466602869A799EBDF9B680E8EAF0" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs", + "bytes": 4737, + "sha256": "6C766B29C8664D15CB113D2CC71BBA9B60AE4CCF7E44C9C5D3C41E1DC4B7ED90" + }, + { + "path": "crates/oa-desktop/src/source_navigation.rs", + "bytes": 3938, + "sha256": "C1DA10882F35F8EB0675E68C642C9A209DDE09679CDED414CFB9903A4E49EC77" + }, + { + "path": "crates/oa-desktop/src/source_tree.rs", + "bytes": 21816, + "sha256": "4D55E0110A1EB128A5D26C28F3AAE5853D79B7245812B7C82BD275F38440A5AA" + }, + { + "path": "crates/oa-desktop/src/source_workspace.rs", + "bytes": 15522, + "sha256": "30F63A6B8D65A38D66061F90D864B5746CBACC0FBD65ABC028A8F3071C47D358" + }, + { + "path": "crates/oa-desktop/src/style.rs", + "bytes": 3015, + "sha256": "05D61BEC16141612CCAFA0988E8C4827F41754509EFDB3B961EAB69127DED896" + }, + { + "path": "crates/oa-desktop/src/tests.rs", + "bytes": 58557, + "sha256": "B017218195EEA2C96ABA12EBA8A99A38945C8CA910A652E272D34883BDB98691" + }, + { + "path": "crates/oa-desktop/src/view_copy.rs", + "bytes": 9043, + "sha256": "5630F31C942E1226AAFD67EABDACE6A5C5494875BDC6F7F077019E8110BA41A9" + }, + { + "path": "crates/oa-desktop/src/views.rs", + "bytes": 48347, + "sha256": "7148E01E8479C2294093159969BC33B9FF0CF916799D481681811C4DE5C60E53" + }, + { + "path": "crates/oa-desktop/src/views/traceability.rs", + "bytes": 7501, + "sha256": "4C89DCF35C25E737D8CCE4D543A5989CA104A5678E6CF40DF47E457773D2EF94" + }, + { + "path": "crates/oa-desktop/src/workflow.rs", + "bytes": 82963, + "sha256": "D9E7B11A86FD3EC79D91F0C9BC9CEBD5A2043CDAA7261622454FB7730AD8E060" + }, + { + "path": "crates/oa-desktop/src/workflow/communication.rs", + "bytes": 11084, + "sha256": "34D121AD97706012722E4BB6210072ECC3A5B4718B0CEAB0C3D0F14D4CD780A1" + }, + { + "path": "crates/oa-desktop/src/workflow/recovery.rs", + "bytes": 9859, + "sha256": "0849B0F34DBA5E8761BE5FA05784D134AC74E4E9DB395D6B9B7D6CFF71E3F3DC" + }, + { + "path": "crates/oa-desktop/src/workflow/transition_tests.rs", + "bytes": 8553, + "sha256": "589B1B2DBADCA74A52C2CFCFD4FE620BD4790ABD113AC02182E91616C52D793C" + }, + { + "path": "crates/oa-server/Cargo.toml", + "bytes": 778, + "sha256": "527FF5E15010A8B560A6659AA1AE4BF0C8DF0D9E019F03C47529EC6A750C0982" + }, + { + "path": "crates/oa-server/build.rs", + "bytes": 45, + "sha256": "AB1BE6AE1FD8BF3EBAEEF6348347A9D5E666BDE693126F199E041659E0B094A9" + }, + { + "path": "crates/oa-server/src/error.rs", + "bytes": 5610, + "sha256": "93817F1502386E4662C96EE1C7D1C4AFC4E33BDA6FD64E46C86C8E1AFD5A9368" + }, + { + "path": "crates/oa-server/src/lib.rs", + "bytes": 7743, + "sha256": "20BEF350350ED6339588B23AF684ABF745F97916C7EDF3853EE042ABE687E664" + }, + { + "path": "crates/oa-server/src/main.rs", + "bytes": 4163, + "sha256": "D59A6A2B5C7034BD2E00A516D2D49ABB16391BF0316C1C357DDAFB9C82D2D1E3" + }, + { + "path": "crates/oa-server/src/preparation_http_tests.rs", + "bytes": 5523, + "sha256": "454B90D0055D35644C8D990CD7576DDF675A9B271CA07FE9B0EFCE25D8DDBC07" + }, + { + "path": "crates/oa-server/src/routes.rs", + "bytes": 46053, + "sha256": "3F9A0C22043DBF28F7A8E587D2FC3ABF869F76F4D23A46FE0AEB9C4392454A4B" + }, + { + "path": "crates/oa-server/src/store.rs", + "bytes": 25524, + "sha256": "37DCFB845B7993D5BCED2BCF25FEDFCD2A9C20A748F9315AFE6CCA1F41F89AE5" + }, + { + "path": "crates/oa-server/src/store_preparation_gate.rs", + "bytes": 4440, + "sha256": "427C390DA3C0281BA1FE115A93EA90800E3B45FCE6BCB9185E71982DDD885FEC" + }, + { + "path": "crates/oa-server/tests/http.rs", + "bytes": 71892, + "sha256": "50DAA6A90E96AD6FE3C6A27DF2EAEBE486270599C203892D09769C547A194975" + }, + { + "path": "crates/oa-server/tests/http/communication_limits.rs", + "bytes": 5790, + "sha256": "BA5474DD8E494A0176FFFB4548C8E3DEB2081B15DE33BFCE82394C764E0A2C16" + }, + { + "path": "crates/oa-server/tests/http/data_contracts.rs", + "bytes": 25949, + "sha256": "8CD456DB83F301BACA380ACF025A5A99DB4EDF7D8839537DFBD02C4307E10E6B" + }, + { + "path": "crates/oa-server/tests/http/port_anchors.rs", + "bytes": 15086, + "sha256": "7199D0A208DDAD9E1518B6E3FB390D64DA111716D7505BA5C995DEAE19AD1258" + }, + { + "path": "crates/oa-server/tests/http/semantic_transitions.rs", + "bytes": 22832, + "sha256": "856D1C9B633FFB581E3E631F4E5A607789A02181DBF060667F208A569BFA4920" + }, + { + "path": "crates/oa-server/tests/http/source6_views.rs", + "bytes": 13023, + "sha256": "867BCCDA0E030EEEB9976B4CEEB47B21AB5044D700278022D5FB495BBB64CA07" + }, + { + "path": "crates/oa-server/tests/http/source7_contracts.rs", + "bytes": 11000, + "sha256": "0E0A467186DA7263C0754890AFF69EBD0D92D36B5224E4865BB6854B18DAB7E8" + }, + { + "path": "crates/oa-server/tests/http/source_projects.rs", + "bytes": 30907, + "sha256": "8FF580F317EC267D1CD72AE14D8638F74A77CDF99CA4C78F66F4290C2D77460A" + }, + { + "path": "crates/oa-server/tests/semantic_http_contract.py", + "bytes": 7251, + "sha256": "4737FF94F10761F94151A694414C5C62737E0391EC820BCA132830652BC4CCA2" + }, + { + "path": "crates/oa-server/tests/semantic_schema.py", + "bytes": 25111, + "sha256": "CACCA116C42FFBC097F1419C0F89921FA0DFD3FAA864D46820FF744CFE6967F9" + }, + { + "path": "crates/oa-server/tests/state_representation.rs", + "bytes": 12241, + "sha256": "6686C826F28248AE4B63EC3F2112E193D4A347724B87974483217FCFEE49CD61" + } +] diff --git a/harness/experiments/canvas-union-windows-20260911/compile-shared-06ec962.log b/harness/experiments/canvas-union-windows-20260911/compile-shared-06ec962.log new file mode 100644 index 00000000..2ae745e5 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/compile-shared-06ec962.log @@ -0,0 +1,36 @@ + Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop) +warning: type `traceability::Event` is more private than the item `app::Message::Traceability::0` + --> crates\oa-desktop\src\app.rs:339:18 + | +339 | Traceability(crate::views::traceability::Event), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field `app::Message::Traceability::0` is reachable at visibility `pub` + | +note: but type `traceability::Event` is only usable at visibility `pub(crate)` + --> crates\oa-desktop\src\views\traceability.rs:12:1 + | + 12 | pub(crate) enum Event { + | ^^^^^^^^^^^^^^^^^^^^^ + = note: `#[warn(private_interfaces)]` on by default + +warning: type `traceability::State` is more private than the item `app::App::traceability` + --> crates\oa-desktop\src\app.rs:467:5 + | +467 | pub traceability: crate::views::traceability::State, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field `app::App::traceability` is reachable at visibility `pub` + | +note: but type `traceability::State` is only usable at visibility `pub(crate)` + --> crates\oa-desktop\src\views\traceability.rs:17:1 + | + 17 | pub(crate) struct State { + | ^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `oa-desktop` (bin "arcadia-studio" test) generated 2 warnings + Finished `test` profile [unoptimized] target(s) in 3m 45s + Running unittests src\main.rs (target\debug\deps\arcadia_studio-e358a5ed59b1de41.exe) + +running 2 tests +test behavior::canvas_workspace::tests::relative_camera_zoom_keeps_the_cursor_world_point_and_rejects_nonfinite_inputs ... ok +test behavior::canvas_workspace::tests::viewport_ordered_batch_uses_each_event_position_and_cancels_before_camera_motion ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 727 filtered out; finished in 0.01s + diff --git a/harness/experiments/canvas-union-windows-20260911/compile-shared-06ec962.result.json b/harness/experiments/canvas-union-windows-20260911/compile-shared-06ec962.result.json new file mode 100644 index 00000000..8c857c19 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/compile-shared-06ec962.result.json @@ -0,0 +1,8 @@ +{ + "source": "06ec9621297627a1f7d5c3bc65efb237d1c9923e", + "start": "2026-09-11T17:48:56.3690729Z", + "end": "2026-09-11T17:52:41.9776419Z", + "exit": 0, + "inputs": 281, + "command": "cargo test --locked -p oa-desktop --bin arcadia-studio canvas_workspace::tests -- --test-threads=1 --nocapture" +} diff --git a/harness/experiments/canvas-union-windows-20260911/compile-shared-6969c82.log b/harness/experiments/canvas-union-windows-20260911/compile-shared-6969c82.log new file mode 100644 index 00000000..8eb0058c --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/compile-shared-6969c82.log @@ -0,0 +1,62 @@ + Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop) +error[E0061]: this method takes 2 arguments but 3 arguments were supplied + --> crates\oa-desktop\src\behavior\canvas_workspace\tests.rs:268:23 + | +268 | self.renderer.screenshot( + | ^^^^^^^^^^ +269 | Size::new(app.window_size.width as u32, app.window_size.height as u32), +270 | 1., + | -- unexpected argument #2 of type `{float}` + | +note: expected `&Viewport`, found `Size` + --> crates\oa-desktop\src\behavior\canvas_workspace\tests.rs:269:13 + | +269 | Size::new(app.window_size.width as u32, app.window_size.height as u32), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: expected reference `&iced::advanced::iced_graphics::Viewport` + found struct `iced::Size` +note: method defined here + --> C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\iced_wgpu-0.14.0\src\lib.rs:183:12 + | +183 | pub fn screenshot( + | ^^^^^^^^^^ +help: remove the extra argument + | +269 - Size::new(app.window_size.width as u32, app.window_size.height as u32), +270 - 1., +269 + /* &iced::advanced::iced_graphics::Viewport */, + | + +error[E0308]: mismatched types + --> crates\oa-desktop\src\behavior\topology\layout_tests.rs:726:82 + | +726 | let task = app.update(Message::Workflow(crate::workflow::Event::Semantic(message))); + | -------------------------------- ^^^^^^^ expected `Box`, found `Event` + | | + | arguments to this enum variant are incorrect + | + = note: expected struct `Box` + found enum `semantic_review::batch::Event` + = note: for more on the distinction between the stack and the heap, read https://doc.rust-lang.org/book/ch15-01-box.html, https://doc.rust-lang.org/rust-by-example/std/box.html, and https://doc.rust-lang.org/std/boxed/index.html +note: tuple variant defined here + --> crates\oa-desktop\src\workflow.rs:22:5 + | + 22 | Semantic(Box), + | ^^^^^^^^ +help: store this in the heap by calling `Box::new` + | +726 | let task = app.update(Message::Workflow(crate::workflow::Event::Semantic(Box::new(message)))); + | +++++++++ + + +warning: unused import: `iced::advanced::renderer::Headless` + --> crates\oa-desktop\src\behavior\canvas_workspace\tests.rs:239:13 + | +239 | use iced::advanced::renderer::Headless; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + +Some errors have detailed explanations: E0061, E0308. +For more information about an error, try `rustc --explain E0061`. +warning: `oa-desktop` (bin "arcadia-studio" test) generated 1 warning +error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 2 previous errors; 1 warning emitted diff --git a/harness/experiments/canvas-union-windows-20260911/compile-shared-6969c82.result.json b/harness/experiments/canvas-union-windows-20260911/compile-shared-6969c82.result.json new file mode 100644 index 00000000..ad440e80 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/compile-shared-6969c82.result.json @@ -0,0 +1,7 @@ +{ + "source": "6969c825a8340980b6a419524ade3bb687a6051f", + "start": "2026-09-11T17:46:35.5365176Z", + "end": "2026-09-11T17:48:07.8569937Z", + "exit": 101, + "command": "cargo test --locked -p oa-desktop --bin arcadia-studio canvas_workspace::tests -- --test-threads=1 --nocapture" +} diff --git a/harness/experiments/canvas-union-windows-20260911/final-inputs-abffa4b.json b/harness/experiments/canvas-union-windows-20260911/final-inputs-abffa4b.json new file mode 100644 index 00000000..100be404 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/final-inputs-abffa4b.json @@ -0,0 +1,2595 @@ +{ + "source": "abffa4b3f0a859d7439103100f59805804d46e55", + "inputs": [ + { + "path": "Cargo.lock", + "bytes": 119973, + "sha256": "065c23c94908fe499d32ed91d55a70946ed5b08ecfda3b483ec1e635572257bf" + }, + { + "path": "Cargo.toml", + "bytes": 400, + "sha256": "369f5b4b2b60705a94623c8c0b68f8e0b048f263280ce07ae0629ad353797d62" + }, + { + "path": "build/windows.manifest", + "bytes": 354, + "sha256": "9c19c5a02f858ece84c44839ad2b145d9c1d90442da0b15c87fd045f975974eb" + }, + { + "path": "build/windows.rc", + "bytes": 24, + "sha256": "a27385edcae0bcf96f55361056cb7be5f36d76fcddcaefd20647c91959563e1e" + }, + { + "path": "build/windows_manifest.rs", + "bytes": 1460, + "sha256": "168da2c42ecccfebc11ee3e1b455e653a68fe9c6d30a9842a8b9e7441d086c80" + }, + { + "path": "crates/oa-cli/Cargo.toml", + "bytes": 279, + "sha256": "49c567f1af74a826683b4d96845feb3806fd9bcb7dc2767e16b5509d4e5e1cde" + }, + { + "path": "crates/oa-cli/src/lib.rs", + "bytes": 15498, + "sha256": "97bebab63b4a611a1d36ad0c20fe452cbe1985fe3a6fbd39c954603da11410fc" + }, + { + "path": "crates/oa-cli/src/main.rs", + "bytes": 2984, + "sha256": "de964f9b6c59c0f31c080afbd3b31d442b07b1b44f6a187a248d3409c45d35fb" + }, + { + "path": "crates/oa-cli/tests/source_cli.rs", + "bytes": 16600, + "sha256": "a3a1b856c7a9fd3d80bc8074150a24fbb0ef0e9ddf736c7cea036725fc3dc2b8" + }, + { + "path": "crates/oa-core/Cargo.toml", + "bytes": 408, + "sha256": "6b5c062474ebb62d5082e07dabed99ee85e4a833edf26690fb4f887028d3416f" + }, + { + "path": "crates/oa-core/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-core/examples/generate_demo.rs", + "bytes": 795, + "sha256": "e26f432c95331aeca50985dbb3ad2c558affd43023b585811f0f7bfaa59f6661" + }, + { + "path": "crates/oa-core/examples/generate_release1.rs", + "bytes": 1798, + "sha256": "1d5f11165d74893e4bc18dbf3a2f2111f4026bd89fd06ee7f0bae69e45c4d4b5" + }, + { + "path": "crates/oa-core/examples/release_dense_views.rs", + "bytes": 14664, + "sha256": "a83158488ef9b7394f136c52b1496719be935c4e5bdfe457cab5c9894f5f9412" + }, + { + "path": "crates/oa-core/examples/routing_workshop.rs", + "bytes": 17248, + "sha256": "571307b18b6d4fa04700c5a4d4b0c2154c419e93a21292b2028e7b6f46a2faa7" + }, + { + "path": "crates/oa-core/src/analysis_numeric.rs", + "bytes": 3735, + "sha256": "6c88eef3119162aeb6c07b9e75812c3af9fbfe75554b0503ad00f103cb478408" + }, + { + "path": "crates/oa-core/src/analysis_numeric/dimension.rs", + "bytes": 3045, + "sha256": "d4ad8e5307a1620e31791d724a0e31fdba860d00dc83e3e1f2209d9c5e22a340" + }, + { + "path": "crates/oa-core/src/analysis_numeric/exact.rs", + "bytes": 17338, + "sha256": "4eb8cb0f66b10e5da204899af82ee26adfec74bab47b1a6c9842b5488ee04c4d" + }, + { + "path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json", + "bytes": 6085, + "sha256": "3b959dec5e100a6f03817f5314bef497347ba579ebd235476516d4bd83399181" + }, + { + "path": "crates/oa-core/src/analysis_numeric/meter.rs", + "bytes": 7129, + "sha256": "1f7174beb992935b4c399acdef91114af73faf041070feab968455ac827e8ee4" + }, + { + "path": "crates/oa-core/src/analysis_numeric/quantity.rs", + "bytes": 12257, + "sha256": "ead5142d6eeea8f99399d57e02d8f7d819980923de76387cd454694d6a747aee" + }, + { + "path": "crates/oa-core/src/analysis_numeric/tests.rs", + "bytes": 27486, + "sha256": "323ce82fc057d36f44463569fd8ea324c7d700fddfbf32bce15e04bd2a66970b" + }, + { + "path": "crates/oa-core/src/data.rs", + "bytes": 42696, + "sha256": "7f9c7c7dd8353cb5a38597b92cde6d3e890203d392473187eb387d41ff37414a" + }, + { + "path": "crates/oa-core/src/demo.rs", + "bytes": 20657, + "sha256": "ad62123f96c20ff023cafc1a9849b0fa06bc62edd1fb9701799e4599b458600c" + }, + { + "path": "crates/oa-core/src/language.rs", + "bytes": 99938, + "sha256": "638f08a42e9cf4996b5d07f69b4d6f517a518369f3604d25be1006e40e080765" + }, + { + "path": "crates/oa-core/src/language/semantic_source.rs", + "bytes": 7326, + "sha256": "584b5bb59d87a7b8d44d5dd2d124c3a1323b21492b138a17bc9526c0087c9e29" + }, + { + "path": "crates/oa-core/src/language_nesting_tests.rs", + "bytes": 20639, + "sha256": "bd061611e8cb6f28ab203c73e544ac98bf275eb464cd314ad5fbd25f773df33b" + }, + { + "path": "crates/oa-core/src/lib.rs", + "bytes": 549, + "sha256": "ea1d9bf5acba8381ba204db12ff5b5fe1f320417d17af517e46b90fd6bcb241c" + }, + { + "path": "crates/oa-core/src/migration_v2/README.md", + "bytes": 1515, + "sha256": "fb61d3faa33d7bdfba73415415a0882c0fbd3e0b45df42f972a19aec6da927b9" + }, + { + "path": "crates/oa-core/src/migration_v2/adapter.rs", + "bytes": 9490, + "sha256": "66741b4c8354a23b8a932ecc5bf1bc3bb6e74279558757b8c35bc2157a7e51b5" + }, + { + "path": "crates/oa-core/src/migration_v2/mod.rs", + "bytes": 1158, + "sha256": "857d8bb13b6814daba6780ca770c16352943c01cb130245d055f11b3b957b3e9" + }, + { + "path": "crates/oa-core/src/migration_v2/model.rs", + "bytes": 52059, + "sha256": "8ac63eb59b2e57bdecff2477ee0634096090924d3dd1851e1c454880a889dacb" + }, + { + "path": "crates/oa-core/src/migration_v2/references.rs", + "bytes": 13369, + "sha256": "33a6819abc83139fdaebeebd1b553238a328637106cce7dd5a535f1791d43e20" + }, + { + "path": "crates/oa-core/src/migration_v2/source.rs", + "bytes": 21946, + "sha256": "74fee9b24e7c912182c998ab9d9aeec125e2009c3214959229e844c67afe2bf6" + }, + { + "path": "crates/oa-core/src/migration_v2/state_validation.rs", + "bytes": 15769, + "sha256": "e29ef071bab0a68460ddf55c46131c6f66c022d52c3c602c37c013c3649da1b0" + }, + { + "path": "crates/oa-core/src/migration_v2/transitions.rs", + "bytes": 704, + "sha256": "f74174c532ba56b57c80b719a11fb4adea0a78ccc2828fff72fe2b9eda5a10b1" + }, + { + "path": "crates/oa-core/src/migration_v2/validation.rs", + "bytes": 98736, + "sha256": "6d5e4a4e2805ae5045bf789a0485e14a61c63346587bace2b4a91173a688f780" + }, + { + "path": "crates/oa-core/src/migration_v3/README.md", + "bytes": 2128, + "sha256": "3755a63f840d7f0eadf68afa7a9d587877a9468ff982d3610539e8a0bb067a82" + }, + { + "path": "crates/oa-core/src/migration_v3/mod.rs", + "bytes": 23023, + "sha256": "f4edb6a0b50ecfe8670973776d703047b064420cb1f05913129c00fc656b3722" + }, + { + "path": "crates/oa-core/src/migration_v4.rs", + "bytes": 4020, + "sha256": "50c652964f0f309e7f3f54b6ef12c40d42fcecc0490f005995c713df1179af70" + }, + { + "path": "crates/oa-core/src/model.rs", + "bytes": 66352, + "sha256": "4585c895c64c4a183941c92cf047a9e52ff0cbe41971e16e78d229ea9dc101fb" + }, + { + "path": "crates/oa-core/src/operations.rs", + "bytes": 11405, + "sha256": "a5ffbbf54cd440316a106d1188bfb699b57dae59f466edcb33618c333cd19f41" + }, + { + "path": "crates/oa-core/src/persistence.rs", + "bytes": 31774, + "sha256": "1c01474f906a63e552c0d590b272c95821c00a100b3e79e58e67dbbeac341a14" + }, + { + "path": "crates/oa-core/src/ports.rs", + "bytes": 45413, + "sha256": "5cac12eea70a54ebebbd6be77b63ce92e3195325c4bc7d27705c518686cbac80" + }, + { + "path": "crates/oa-core/src/projection.rs", + "bytes": 21374, + "sha256": "69ed12bace2cc5024e151752a174ab14034ac2d05eb741fcf13e9a27521c059b" + }, + { + "path": "crates/oa-core/src/projection/port_layout.rs", + "bytes": 13474, + "sha256": "a08d17d10a58bb8b206dd001b19b91d5aa3a9f743b298a4da57d503223c20079" + }, + { + "path": "crates/oa-core/src/projection/state.rs", + "bytes": 5233, + "sha256": "73a607bac713ec7ff49a509fefb1a53c7686dca93ee241ffc835564d743afb28" + }, + { + "path": "crates/oa-core/src/references.rs", + "bytes": 21957, + "sha256": "93553c234f32da35f1f41a7b83ad81c471c9803efa78ca222cf7de3c2f86c0bb" + }, + { + "path": "crates/oa-core/src/routing.rs", + "bytes": 50309, + "sha256": "c5ebb350ba76f731f5c48d968bfbba4b6ef68ec046eb5893d116c4637ef15080" + }, + { + "path": "crates/oa-core/src/semantic.rs", + "bytes": 540, + "sha256": "f9895dd5b8fb27d10d4cfbb41c3542ef2018365a589b8d5778e5674ae5a1f239" + }, + { + "path": "crates/oa-core/src/semantic/catalog.rs", + "bytes": 49407, + "sha256": "1ff74fcca18084d178681d7ff38ee1ccac632c27c4de0595ba1ceab8067ab3a4" + }, + { + "path": "crates/oa-core/src/semantic/historical.rs", + "bytes": 15271, + "sha256": "eaf3284bf234312698514cdb636bc8348e1b5d746cb16b18df777e77d312abfb" + }, + { + "path": "crates/oa-core/src/semantic/linked.rs", + "bytes": 6035, + "sha256": "6f335b14e809aecfc35defcaf77cb821b080d1506dbc46297912b25af98c07a2" + }, + { + "path": "crates/oa-core/src/semantic/mutate.rs", + "bytes": 22683, + "sha256": "dc2b4c4a6eb156fe468eaa8c85fd37a505c0cd8ea1f226cb69cd01689afd3d9d" + }, + { + "path": "crates/oa-core/src/semantic/review.rs", + "bytes": 51116, + "sha256": "0c157d2c327e72e89cca3b423e71ee73798105c5bb7271439c1a101642c91e3e" + }, + { + "path": "crates/oa-core/src/semantic/types.rs", + "bytes": 10405, + "sha256": "6922fe35089f12fad43ab0ce1c6886d01a3855f90b8e2543e99802ac5e6f1bbb" + }, + { + "path": "crates/oa-core/src/session.rs", + "bytes": 35237, + "sha256": "1eb77b329b2986fc6f5dc0171e77fd860375cdfd8c769a1367b445d6c49ebe00" + }, + { + "path": "crates/oa-core/src/source_files.rs", + "bytes": 74803, + "sha256": "669491dbddb8e8e6556161426dffa1a49ac1c826d31020bff0956d81a4d84683" + }, + { + "path": "crates/oa-core/src/source_project.rs", + "bytes": 61843, + "sha256": "1394370bcc5908873b830c2c11312e6e1611af41f46c4f43ca0a6bcb713bb466" + }, + { + "path": "crates/oa-core/src/source_project/source_migration.rs", + "bytes": 4700, + "sha256": "add463ae85c420f853d324b00a221f0f874164cc3f3ed6bbb07557a9ed7a8c95" + }, + { + "path": "crates/oa-core/src/source_project/source_structure.rs", + "bytes": 20225, + "sha256": "9f1f83315856a7025fe662c0fbb13452f31613224a03663150d6225d1ddc162f" + }, + { + "path": "crates/oa-core/src/state_validation.rs", + "bytes": 15665, + "sha256": "e28d7f74385e2a78763716b34491bd73a00bfeccc085970ceb130627e3faee18" + }, + { + "path": "crates/oa-core/src/topology_context.rs", + "bytes": 13734, + "sha256": "16de69b297de95d93c73808621607833b713af811dd082f0169186ae1e5c3e2d" + }, + { + "path": "crates/oa-core/src/topology_context/analysis.rs", + "bytes": 20036, + "sha256": "dead6194f0aa60cc46045c0e5ee10dbdbb689158c38634dfbbbc45d2ba88ac65" + }, + { + "path": "crates/oa-core/src/topology_context/analysis_tests.rs", + "bytes": 5258, + "sha256": "17839263410815e96fe027d4eba3aae0a9ab3b82b16947e2b29ebe0d09782b56" + }, + { + "path": "crates/oa-core/src/transitions.rs", + "bytes": 51998, + "sha256": "fadd1b07b89cae4117b91abb4f1ac9ea81ff818ef20f2175e76ee8cf21fb708d" + }, + { + "path": "crates/oa-core/src/transitions/communication.rs", + "bytes": 8634, + "sha256": "2b476630a88edd9021b9156ca12a5402e01604095f0d4002afb452671903a028" + }, + { + "path": "crates/oa-core/src/validation.rs", + "bytes": 112792, + "sha256": "7c1b4ae5e4ebba13e99af9158ccde138dfc5e9a7afb580e76fba72d0b64ba74e" + }, + { + "path": "crates/oa-core/src/validation/coverage.rs", + "bytes": 6478, + "sha256": "b08e4b344ecad055714168c83beff7fe29ba4edc53edbffa2eae04e5580a4d5f" + }, + { + "path": "crates/oa-core/src/view_options.rs", + "bytes": 24481, + "sha256": "421ede0a04c593ca1df0e6e0d06fabcfcc8113cdbcfb0adc7f5c8ef4d1724208" + }, + { + "path": "crates/oa-core/src/view_options_tests.rs", + "bytes": 21433, + "sha256": "64aac44de2df42a1012239a3dd75c473daf0c8990b3170b7bf7e78a556080208" + }, + { + "path": "crates/oa-core/tests/communication_boundary_scenarios.rs", + "bytes": 8816, + "sha256": "e1100b8f363910a487d90c73acc7df543dc6d532158c3c63ad11cd3d40d8e12b" + }, + { + "path": "crates/oa-core/tests/communication_means.rs", + "bytes": 18102, + "sha256": "a25f2749180bc6f4beb1445bfad3351eedf0669131a7439b633ab804439e95a4" + }, + { + "path": "crates/oa-core/tests/coverage_diagnostics.rs", + "bytes": 20527, + "sha256": "e2e9c3996daab47f2bd23f5d8c48b4e372af33331152f5fc85b76f06d04fcdf4" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia", + "bytes": 29220, + "sha256": "f0df129c1914bfc01d0b047b0cd4b520d8cf0c249ea9ced085de400c0be87ff3" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json", + "bytes": 43990, + "sha256": "3ed53508eb544bbd788832f7bebea2ec31b3d66191f24f45098cc9750ec4c005" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia", + "bytes": 2450, + "sha256": "305f19d1b79f99f8d4cf627ba2e9ca6836bb7959cc0070227815937a699eafea" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.json", + "bytes": 8240, + "sha256": "1b41299c53204e6232397d25a58d5bccdbbc16e770c75e8edb55ce36ccfe58a2" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia", + "bytes": 1222, + "sha256": "5fa53a26a5ad010c8e59eb56d9b2b52003569e66c0377ffb58840590168ad0c1" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json", + "bytes": 1734, + "sha256": "2808088afcc165f4bd6da5129151143411c997041c3de61d600ab76e05d24a47" + }, + { + "path": "crates/oa-core/tests/language.rs", + "bytes": 56482, + "sha256": "77f6cb2a69976bfb5f60b243b71c07fb0edc7505002c1e9e73fcd6ddf6d9496e" + }, + { + "path": "crates/oa-core/tests/port_anchors.rs", + "bytes": 26893, + "sha256": "efbc0fa18733b1aaac501f9785a4c392fc49c05c45a5778d26e9b5dd951ca6fa" + }, + { + "path": "crates/oa-core/tests/port_migration.rs", + "bytes": 15620, + "sha256": "1801c376a5435ac7efb2a8342348b2c7ffef930bba65deca4acb66b06e0483d7" + }, + { + "path": "crates/oa-core/tests/port_source.rs", + "bytes": 4858, + "sha256": "8d2ddaca4045d330f6eda2591561497b858aad53769a823249d0bea4f5916e12" + }, + { + "path": "crates/oa-core/tests/prepared_commands.rs", + "bytes": 9555, + "sha256": "52cab1c668064777412df0a6a3c20d74d579b33794b4b00525b9f151c06eac9b" + }, + { + "path": "crates/oa-core/tests/process_diagnostics.rs", + "bytes": 1104, + "sha256": "1dbbc590239ef7542be4a2bdc9543aaa64d108417b03dd312a0894e364745906" + }, + { + "path": "crates/oa-core/tests/r3_constructor_ack.rs", + "bytes": 24163, + "sha256": "e5c44676ea0aab97f2e091ddac2f4bc4a4a90f5877f7bbf08b61229600944dd0" + }, + { + "path": "crates/oa-core/tests/r3_migration.rs", + "bytes": 6590, + "sha256": "ebd04120f5215b3956fdc79ee78f6e38734c7d13392f1fc28aaba957912265bd" + }, + { + "path": "crates/oa-core/tests/r3_semantic.rs", + "bytes": 77473, + "sha256": "49c3763eaf2131070a7c46cc23a84f917d292d41610b6404510e5607f8890c32" + }, + { + "path": "crates/oa-core/tests/r3_source.rs", + "bytes": 15311, + "sha256": "f31f60659df3c7ef601650d1cff140589d8729f01a3f65a4a6a6c2272f15c408" + }, + { + "path": "crates/oa-core/tests/r6_data.rs", + "bytes": 41437, + "sha256": "5f7ffb3bc73c3c85423623fe56f8680646dc97040327bdbead9d47a37e80f155" + }, + { + "path": "crates/oa-core/tests/r6_migration.rs", + "bytes": 9174, + "sha256": "81519bc1512f5e069432e9a1cdad04c10216d492df01b4b75b02102480ebd6a1" + }, + { + "path": "crates/oa-core/tests/r6_source.rs", + "bytes": 9519, + "sha256": "88a0aa087a7678e2898ff6057538072ca92072190712f05fff08372cc56ebed6" + }, + { + "path": "crates/oa-core/tests/release1_teaching.rs", + "bytes": 10010, + "sha256": "be179290d871125dc97cfd2a3067c7ac89c8a538fdb0afafc5b825bcdfb2bef2" + }, + { + "path": "crates/oa-core/tests/release_capabilities.rs", + "bytes": 19422, + "sha256": "b92cba2a562e78287ddd7dc3e94557848d17faa0c8a102601b50286b9af48a30" + }, + { + "path": "crates/oa-core/tests/release_diagrams.rs", + "bytes": 7689, + "sha256": "cefcc0582d91266d5de6ee5daefcfe41b606ab971ab235b59034a8e1dbc24b7d" + }, + { + "path": "crates/oa-core/tests/release_language_limits.rs", + "bytes": 5204, + "sha256": "8578da4f76bbd2d0e1bfe295ab1259e2162a3fe3d0d2ba8901915d7a40942806" + }, + { + "path": "crates/oa-core/tests/release_ports.rs", + "bytes": 32740, + "sha256": "506e62beab0c7ec0cece83e9e0c1a810fa1f99285c9e1cab0c77f06de55a40e1" + }, + { + "path": "crates/oa-core/tests/release_routing.rs", + "bytes": 23165, + "sha256": "ae168faaba23769b7ca1fd65f217b9db671726e67276eb5a04e5e72b1447a462" + }, + { + "path": "crates/oa-core/tests/release_scenarios.rs", + "bytes": 41067, + "sha256": "0f5d876badeec653fc7b12610a77d867e9a53582e605253deff8d379da21572f" + }, + { + "path": "crates/oa-core/tests/release_states.rs", + "bytes": 26873, + "sha256": "b1a26acee95eaba7f9920f9da738a1e33606552eb078e6d298895daff7e3d811" + }, + { + "path": "crates/oa-core/tests/release_topology.rs", + "bytes": 17643, + "sha256": "c51dbe5aa5a038f9ed830daa02aa67714472179eb7fd6da5ce36dc6931c8b3a9" + }, + { + "path": "crates/oa-core/tests/review_regressions.rs", + "bytes": 21823, + "sha256": "f154e5c88d35e31512e2b07733a1327ea3e2fcfd1ef8eb23066a4e9573f387c1" + }, + { + "path": "crates/oa-core/tests/route_source.rs", + "bytes": 7270, + "sha256": "1a73fdbac0d0ae0e84d8d67b6ad398e11223958f5dca007f73757c91827decfb" + }, + { + "path": "crates/oa-core/tests/routing_geometry.rs", + "bytes": 31890, + "sha256": "2d8eea4a2bba535ed0ea6daabccdaf87b375828c404baa10518c7bd7fd6253d3" + }, + { + "path": "crates/oa-core/tests/routing_labels.rs", + "bytes": 5530, + "sha256": "dff884d799c3c5c9257210248c55767e294e3cb317c605cbd0ddc9600c216eb6" + }, + { + "path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs", + "bytes": 4640, + "sha256": "d93eaf9667f4221232168bef0fe9f069e5be1b1f23410bda5dd67010b96e5c8a" + }, + { + "path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs", + "bytes": 14957, + "sha256": "7f526d363ba05e7ac89faf0fead0796e419c34ee204edf44a1b46c78308b0970" + }, + { + "path": "crates/oa-core/tests/source6_view_contract.rs", + "bytes": 14307, + "sha256": "fa566de53946d40bbbd9033edfeed177180c4dc44afa2f2f4667ffdc622eb3bb" + }, + { + "path": "crates/oa-core/tests/source7_contract.rs", + "bytes": 43262, + "sha256": "cad4f1a135ddbd947bdf3c71d1b44f1e438725095ea8c4d893464bc721c2363f" + }, + { + "path": "crates/oa-core/tests/source_files.rs", + "bytes": 13168, + "sha256": "f0429a0b89a443458ea9af7083c1e4e7411417a7e6b47e618bfbb9266dc164f9" + }, + { + "path": "crates/oa-core/tests/source_identity_updates.rs", + "bytes": 5139, + "sha256": "f52aa38b4d317e532c641ad7463472e4b40e802e60052cf0017556ffc63da06c" + }, + { + "path": "crates/oa-core/tests/source_nested_updates.rs", + "bytes": 16892, + "sha256": "9ac3390936c0d250651ea94a1b8faf0adfb680f061ececc56f2a52909b5ba855" + }, + { + "path": "crates/oa-core/tests/source_project.rs", + "bytes": 45910, + "sha256": "cef58adb16aa4d2527ddc0628aec3e26f0b42b4be70014a7e360fafdac13a942" + }, + { + "path": "crates/oa-core/tests/state_diagrams.rs", + "bytes": 23973, + "sha256": "6b02b387a3ae559cb3ad72de24292a5abcb76aaac0adbab5d2aa5211b22cd2e5" + }, + { + "path": "crates/oa-core/tests/workflows.rs", + "bytes": 33426, + "sha256": "bd843dfddbd5724ec08f657571f887496203056fe2fa5864ef50a168a613b89b" + }, + { + "path": "crates/oa-desktop/Cargo.toml", + "bytes": 842, + "sha256": "f3547b2b924d5db6706c0a8e069014b440ff65315d82fc8aced0360af4b3cdc9" + }, + { + "path": "crates/oa-desktop/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "aa2e4853b25bb7e185b1de49eda42028523c8ebf9599bfe6b7cb2a2b2a688090" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring.rs", + "bytes": 27835, + "sha256": "37c7db33e99dd4adcc5a51f09ee4878af94fd32413359890a541690080885f65" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/palette.rs", + "bytes": 8887, + "sha256": "8f9937cb30b48b83759fd084a8a5990a2daa3920280f1210abe048560241985a" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/tests.rs", + "bytes": 14732, + "sha256": "ed864839940a1910c97537b6ee151b57ab115ece6f60bae602552b84f9af4a5d" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring_tests.rs", + "bytes": 18599, + "sha256": "77eb1a0038d038b989f7ac568760e16fe23c4b8d73cd9fbe32b70817ea23cfec" + }, + { + "path": "crates/oa-desktop/src/app/behavior_lifecycle_tests.rs", + "bytes": 13669, + "sha256": "1af2e290aa259a6af6b0585950cda93fe3a562c7b8be1e938fe1d80747b11808" + }, + { + "path": "crates/oa-desktop/src/app/checkpoints.rs", + "bytes": 29576, + "sha256": "55e5babdcabef767af3bc8be9cc3153b3f5cc0695701b890820ec0225643852e" + }, + { + "path": "crates/oa-desktop/src/app/diagram_selection_tests.rs", + "bytes": 5582, + "sha256": "4fa507adbc23db3a6eb202d809c695ec0ebd6181356930c4fa7528a9a0c7be57" + }, + { + "path": "crates/oa-desktop/src/app/hosted.rs", + "bytes": 25059, + "sha256": "2f7094c078e1d7cbccd6c0d310afa6c400a7e031f12918bbfce65ba62754e316" + }, + { + "path": "crates/oa-desktop/src/app/hosted/tests.rs", + "bytes": 30344, + "sha256": "fa118f64d04b400ccc01e4e67adaf3541975288882afa265a9e7f092cdbda232" + }, + { + "path": "crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53d4a422d3066e07cccb9e8ef2ad08ea95e451b9b7cbef29a27e6634e035ed97" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests.rs", + "bytes": 20067, + "sha256": "b96ddc6bff7f8fde97720e18a92070f891c1340348d2e19c7c9c5a7bda650ce8" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests/mounted.rs", + "bytes": 32805, + "sha256": "14a4914a1c86e50c395672dab8fe80d7af679a79c9082890b509fad7b7c543d9" + }, + { + "path": "crates/oa-desktop/src/app/review_navigation_tests.rs", + "bytes": 7049, + "sha256": "872fa91479dad5436e139ffcaece270323e9f5daeeb7a4d80c2c2534f48bc21b" + }, + { + "path": "crates/oa-desktop/src/app/save_as_tests.rs", + "bytes": 4113, + "sha256": "f8b915793f1a36fd152569c4781a4bdc17f4af404a8ceb373330053413087a1c" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram.rs", + "bytes": 9979, + "sha256": "f7fd50cc9a86adb794713958348af63afc7c698f74965e13188b7b8a9e735c55" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram_tests.rs", + "bytes": 39957, + "sha256": "612e414b1a2cab12cbfd8fce23745559ff27577700694d53af67ad1f2883ee2c" + }, + { + "path": "crates/oa-desktop/src/app/transition_context_tests.rs", + "bytes": 11508, + "sha256": "47002fb025f3a2f8c53871f9509bfe8611a61db6fb0e1b3125eb6e875425cb1d" + }, + { + "path": "crates/oa-desktop/src/app/view_copy_tests.rs", + "bytes": 7846, + "sha256": "296821c51dcde568e7d717039aa26bfc1b56a9074aa16051007bb351b33a4d44" + }, + { + "path": "crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5e7cecb54bfca64e4462f7881f460900abd925ba2cfe9451ce2b0c78af4bc850" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7758, + "sha256": "9303ed4f74ee0317e4abeab134f7c31f50eac5eb5d85940d771cd3ad595258fa" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7804, + "sha256": "2b8d97b8c0e3ff1b1a2c32187b1d0eb25edf39ba582b5fb3303bc880f674f7ba" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "cc58cb3838f546150d22fe4dd83a3ffd52504c279febaeb8d0b4a6a3b8a73950" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 27020, + "sha256": "d84d022574a6f1d69f203d9383502227fb9fc23605c6fb48779ca9f09bee546d" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "af1c7c1e802d1207ee6c7bd66b703811a250d7b56511512af6c0945c776e68a7" + }, + { + "path": "crates/oa-desktop/src/behavior/contextual_source_tests.rs", + "bytes": 49386, + "sha256": "5f05318a6533503b9d6febf1ec4dd7256f192394bc1b952a7904c8175bd8ddae" + }, + { + "path": "crates/oa-desktop/src/behavior/creation.rs", + "bytes": 28978, + "sha256": "33ae9aaf4ba90de9432870cc8e99708ef39e3638ae8283a9253c333785c91299" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/app_tests.rs", + "bytes": 13181, + "sha256": "e9c5bf2b07357b7a2f679ae0d8f6904c1c1461aaeb94fa0db478b54d4641ae3e" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/tests.rs", + "bytes": 22793, + "sha256": "a0d1daf9fb3920f7e6181a9802d84fbe056cbf0043b9e41dba5fdda687d9818a" + }, + { + "path": "crates/oa-desktop/src/behavior/form_session.rs", + "bytes": 3941, + "sha256": "f5027fb3a5a5599a182e7d1a10dba56dd66eb1f23a4ba4c4a1744c9a6a5b6e61" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "fb1da03e8df05a59253f7fdf34391e30abd8aea427b7d0a12b86b80ff3bf9eca" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/focus.rs", + "bytes": 9728, + "sha256": "73a9ea10ad7312086081b8688396a21ab4251c9faecc2df6d984b702f3963533" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284f5a7da09bdc77d70c6788a091d57927150c796ffc10d786793cff7da4b7c6" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950cf4347a3edea615b1ac71d9a681c32b446b5109fcfd0f03f3ae2b147360bf" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle.rs", + "bytes": 9324, + "sha256": "758d244c2e8de61f515d540baa4d967275f49720e0253c6c9159eb9acd71c859" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle/tests.rs", + "bytes": 15430, + "sha256": "01b576993b329810cd78a859b45ac52c66f5a14549065661dd1308196e0b724d" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_operation_tests.rs", + "bytes": 10087, + "sha256": "89fb71c690e07eac314f7a5e05862d5c1e2f9c21039673bf22c4ac894d21c977" + }, + { + "path": "crates/oa-desktop/src/behavior/source_draft.rs", + "bytes": 18031, + "sha256": "08190823090bd8b0305809099abadcc0de431175f7044f1d5abdceda52612ee1" + }, + { + "path": "crates/oa-desktop/src/behavior/state_canvas.rs", + "bytes": 93561, + "sha256": "2e2ad57b86f4cc1cc0c4c4c230571ef19bcc2f052001a8bdcc44925e5103077a" + }, + { + "path": "crates/oa-desktop/src/behavior/state_edit.rs", + "bytes": 35736, + "sha256": "cb2289e727163626152fc2f11195c6ee824851cb9442c57496da97fe7aebfb7c" + }, + { + "path": "crates/oa-desktop/src/behavior/state_menu.rs", + "bytes": 7447, + "sha256": "cc87c5caa177e114e4d9a7f7367e37ec2b0d1e1c16b0b99709b05ea0e59cf894" + }, + { + "path": "crates/oa-desktop/src/behavior/state_view.rs", + "bytes": 6988, + "sha256": "1eb892a9139ecb0671784c879ee5114f27e598e5b3589f5cdad0abffd5909f24" + }, + { + "path": "crates/oa-desktop/src/behavior/states.rs", + "bytes": 26986, + "sha256": "8986ed9492960e20f1254e9dfe3ff91d79ce9fef66f3bda738f9933278043954" + }, + { + "path": "crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723e51a7da4eb224d51c1659ebbfa4dc8b11557bc8bf70f6e3e1649bad24449" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual_routing.rs", + "bytes": 16731, + "sha256": "d914b0e82aa8d253fe64be2ef74443d1fd2e6cec26d1cf30620a57646e1d426c" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309b8978ec2586ff4614236c2f624af6d934d117363e917b8c1cf9f2b1295eb" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733f8badafb9ba87e3f861f03f78809450eee72cc118341eb03b24904790144a" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "a990947cb39b2fb15c21dfbc95ee16f92bf6be749d3872c6904762b9bde37089" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "b2d502c11426bffb8763515f2a56aa4c9f8e203bb36d65eb34f81c123fc406a6" + }, + { + "path": "crates/oa-desktop/src/connection_edit.rs", + "bytes": 5301, + "sha256": "020581e077f82b43c936b623cc51733b02b88428df4e5f25df74b9705819fa37" + }, + { + "path": "crates/oa-desktop/src/connection_tool.rs", + "bytes": 27023, + "sha256": "649da93677d52dae495b0e19bc32f3122ab05a6d84de8402578ff5ce3f6ee1d3" + }, + { + "path": "crates/oa-desktop/src/context_menu.rs", + "bytes": 52955, + "sha256": "629354b34dc232763700251f2695b4d4e53d6251c0b1432ecc0dfe424f7dfeac" + }, + { + "path": "crates/oa-desktop/src/data_values.rs", + "bytes": 23877, + "sha256": "e1deb1cad551f0a9fdef9653aad257cfb36e6d9c0f93b6245061ec3fab8fd365" + }, + { + "path": "crates/oa-desktop/src/data_view.rs", + "bytes": 38871, + "sha256": "158b852be5d3db450aa5cda8b68a35bd96bddce1d8d053a7a23582095c06362d" + }, + { + "path": "crates/oa-desktop/src/diagram.rs", + "bytes": 105256, + "sha256": "4580b170e264d14e5cfa874709f6d2e076a05155bfbe1809336a9d05f75e98ef" + }, + { + "path": "crates/oa-desktop/src/diagram/lifecycle_tests.rs", + "bytes": 9316, + "sha256": "d0e72cc0574657d5a31c3ce307b58401d0ed77eedccd1aa02377f55ab0bc03f0" + }, + { + "path": "crates/oa-desktop/src/diagram/pointer_tests.rs", + "bytes": 11833, + "sha256": "10ca5049034159205ac8f22805354f308b0ba0596f331568c26cd1393bc8cedc" + }, + { + "path": "crates/oa-desktop/src/diagram/port_hit_tests.rs", + "bytes": 5624, + "sha256": "d2d0647629bf725b4dddfc211f5a27d495ed093a79af87cdae6c196f09a97b93" + }, + { + "path": "crates/oa-desktop/src/diagram/port_labels.rs", + "bytes": 13408, + "sha256": "28782983b82e4fca5dd4410ac8bd562359943b7010c7b24ecc42f0c7ad7fb63e" + }, + { + "path": "crates/oa-desktop/src/diagram/view_options_tests.rs", + "bytes": 2815, + "sha256": "cf0c98a7715558baf17b59f67a80556f98ad30262b4434635e617c6601775433" + }, + { + "path": "crates/oa-desktop/src/document.rs", + "bytes": 31080, + "sha256": "2ecc2cd496d4a83fd7a32b9dc68e49f0f1cb31a06263dfd6939b4764da5b73e2" + }, + { + "path": "crates/oa-desktop/src/document_save_as_tests.rs", + "bytes": 5781, + "sha256": "83dc243a25f4e97674ff0d86c794a490cc923e98f1f9fb05bb038f622f11e20b" + }, + { + "path": "crates/oa-desktop/src/explorer.rs", + "bytes": 14764, + "sha256": "fa01c59219564ba9ae5553abc2b6f94e9849f9148d5812ef99b40318fe5dcf9a" + }, + { + "path": "crates/oa-desktop/src/explorer_view.rs", + "bytes": 13773, + "sha256": "a720d7484bcc3a24729332a00e005d1cc6f5f945a9cc3965134e0024dffb581f" + }, + { + "path": "crates/oa-desktop/src/forms.rs", + "bytes": 105544, + "sha256": "ea124f479fcd2d3978ac3cb8eb81fec4a7aae5e4f4414c4bdd54849e954dedf9" + }, + { + "path": "crates/oa-desktop/src/forms/context_fields.rs", + "bytes": 10831, + "sha256": "eaacd474f6500dd344d5a8b63ccb1d1dd86fe2aea78ab708afa8a80d62ce69bf" + }, + { + "path": "crates/oa-desktop/src/forms/data_fields.rs", + "bytes": 49285, + "sha256": "a540a0d451bff56a96b1d28643b2e8c0649f3846350c25c5b154729c0538e77c" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields.rs", + "bytes": 27414, + "sha256": "e7100474cde18bdfe95562bcb3c055f29b4b15aa64788a578194464aebcd046e" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields/tests.rs", + "bytes": 33950, + "sha256": "16d3c308138c063f45986d53c621ad83c7dc2ce853edd31b7204e2939a382df9" + }, + { + "path": "crates/oa-desktop/src/forms/port_fields.rs", + "bytes": 30554, + "sha256": "3fe684d651d5be6203fd9db3979c772913e1af5ac1514e67be386ffd88dc8127" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser.rs", + "bytes": 12130, + "sha256": "b60af363be333af048859e51c28e8c989cb5c95d696cad16c8eec15f78d57a12" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/adapter.rs", + "bytes": 10447, + "sha256": "87ab4bc3bf27fd4b5a458791cf62c7ba8e44e9ebf846d8b851b8b6ffb454cd64" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/tests.rs", + "bytes": 27835, + "sha256": "8e98f8a3fcb814d4a201cc85b3db87a689fd3d2ce487f8a2fe312fc296f93377" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger.rs", + "bytes": 13336, + "sha256": "34d0acecfe6bed193970aa473cc3313984971264b86d7b362a6b84c23d013c0a" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger/focus.rs", + "bytes": 9923, + "sha256": "f1e79f4b03b2f9fd1cdc335054d4ae177dc9e759b6ea3d334b2bcca39b57191b" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/widget.rs", + "bytes": 33804, + "sha256": "d66cc17af7a48bb8598be683f084bb08fa7af7304f9636f0a1bea94c5cf6c727" + }, + { + "path": "crates/oa-desktop/src/forms/view_fields.rs", + "bytes": 24987, + "sha256": "5184d438f0dfd4e29f284ff048e19da73e71be1f24a6e9f123c7e922aecb85fa" + }, + { + "path": "crates/oa-desktop/src/guide.rs", + "bytes": 115183, + "sha256": "5432f56753c8f2614ea053accf00cc095b9c591633959c629c5c80936d05e4da" + }, + { + "path": "crates/oa-desktop/src/highlight.rs", + "bytes": 2751, + "sha256": "da7c254fff7052e5fd9ec7469f473b91a819406028d7c304f6e22c4169561600" + }, + { + "path": "crates/oa-desktop/src/home.rs", + "bytes": 17587, + "sha256": "f03c1071334894f586a5a7c86026602a694db3c2faf902822e1dbe5156a859c2" + }, + { + "path": "crates/oa-desktop/src/home/tests.rs", + "bytes": 8167, + "sha256": "3b03ccfc97dfb75954e1e0ed568bb87a632295c07b96cedc05ed381f87b378f3" + }, + { + "path": "crates/oa-desktop/src/hosted/client.rs", + "bytes": 21912, + "sha256": "54b24b4b50f8d87d7c92946ea5a2cff4c98760e70555b3ce3d90be27115e95d4" + }, + { + "path": "crates/oa-desktop/src/hosted/client_tests.rs", + "bytes": 23009, + "sha256": "3ef70907ca75e098d631c41b90badd4cff1e1eb1b3c78e15a9f811c13ba8ffbe" + }, + { + "path": "crates/oa-desktop/src/hosted/mod.rs", + "bytes": 3744, + "sha256": "1b75e11c80fcb38fd20bb3487e3f88729fdac73e471c5ae64da28c249f77c63b" + }, + { + "path": "crates/oa-desktop/src/hosted/view.rs", + "bytes": 9281, + "sha256": "fc0c7b7ce3a5f05c49713df6661067751f81e810af11c614ca265ea9490e6463" + }, + { + "path": "crates/oa-desktop/src/main.rs", + "bytes": 1511, + "sha256": "cb0a919fd0df0924591283da5b55d64d525d8461640441509350be840235e51a" + }, + { + "path": "crates/oa-desktop/src/model_menu.rs", + "bytes": 34680, + "sha256": "12994a5433a91f8295a59d4bf40f35f1edb8d8dbb497a11eae8760299627f1d4" + }, + { + "path": "crates/oa-desktop/src/navigation.rs", + "bytes": 8839, + "sha256": "71355a12e1b4791106426d83fe5ebf0b29bce8e1dc594f0655ab85695469669e" + }, + { + "path": "crates/oa-desktop/src/port_edit.rs", + "bytes": 7809, + "sha256": "70b99f577c4fdeda8780c8dc1ee7b1639fd77aaca06658f06d725139fb3efb6d" + }, + { + "path": "crates/oa-desktop/src/port_preview.rs", + "bytes": 35855, + "sha256": "069487c4ece737d52e8ae4a84dcfb132236aca6799e841c91c8e7244320cf871" + }, + { + "path": "crates/oa-desktop/src/port_workflow.rs", + "bytes": 40582, + "sha256": "ccc15f97c3272145a67bb5857b4faeaf7437561bd61912fa27ab26e8824146a0" + }, + { + "path": "crates/oa-desktop/src/recovery.rs", + "bytes": 21041, + "sha256": "57e22e5fbb0e994e64ae03e69450271fe9f04a951db1670c2a018abe99db4489" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/README.md", + "bytes": 1587, + "sha256": "a90002df1e087d2dac7be26926864243568a0de7c4103e4927435b34178e8016" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/profiles.json", + "bytes": 473, + "sha256": "2c12ae96b50e6da5d60ce2b7fa00204be71dfbe2fd51311fc337a0de6a4332ff" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-bundle.json", + "bytes": 80974, + "sha256": "12715995e6f7f35414aea53b3ad720b9b31873ffdeaf4444231db1a5ac3c463e" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-model.json", + "bytes": 86191, + "sha256": "5535a706df9ec21715e8b6a5aa6dbd79636ae461450aee61305559016167612b" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-bundle.json", + "bytes": 85286, + "sha256": "ea88890a33f47d67dfb06d6dbbcc179058cf28dad76ae7d615c00aaf30dbbbfe" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-model.json", + "bytes": 90629, + "sha256": "5bb8db0b6394276666323483f977ac3586f88554c81a148c6d508fca0ef4576f" + }, + { + "path": "crates/oa-desktop/src/recovery/source5.rs", + "bytes": 6956, + "sha256": "0f0a763f0c98c2c908d4c2b95d8a4c042b6a255ee1cc16260e5404669691f642" + }, + { + "path": "crates/oa-desktop/src/recovery/source5_tests.rs", + "bytes": 12146, + "sha256": "cfd17a742f1f27b523fce7cd9309f0a07a30413395f6c58659fb91d02834a7a3" + }, + { + "path": "crates/oa-desktop/src/recovery/source7_tests.rs", + "bytes": 18213, + "sha256": "7ee61de66c75bf949489ba51ddc9677e2b4efc68801cf27909c9b894bfdc9288" + }, + { + "path": "crates/oa-desktop/src/route_edit.rs", + "bytes": 2941, + "sha256": "457ae59c2761a43a8620b4d336a7cf0f54b965997110ecaf968756a88b6b0374" + }, + { + "path": "crates/oa-desktop/src/semantic_review.rs", + "bytes": 3432, + "sha256": "8dd1e93783d63a663c7e56c28775a5c389cfd7a6b562013d6a95c891afa21ed8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts.rs", + "bytes": 24201, + "sha256": "a79236a8150597ff808597e59cf4f1e9e61c74c4901d9c8cf42d3427bb52a9a8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs", + "bytes": 11034, + "sha256": "3f255d04e4bffa5a467b75e3cbf6f7149aa3269a02f2a89fbaa1db0e5df9ae62" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_tests.rs", + "bytes": 20798, + "sha256": "04d0dd2ac453705185663f705282aa4a3a31433466a5be305764248da1a03c32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_view.rs", + "bytes": 6447, + "sha256": "2100ff73565765d8293c84f6ce2d8e566610c1524c50e32ce3c37dc33b6686e4" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments.rs", + "bytes": 18160, + "sha256": "de0ec0c740895b160ff5b847332c087c3f1a48931583464069470cc61315ed59" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_tests.rs", + "bytes": 29389, + "sha256": "8b26be0324e0f33b1e7cf7af340350df8ed98963002d6da8589ee0e6f6900bea" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_view.rs", + "bytes": 3032, + "sha256": "932f231554d77c7cb119d33bb06b9879db5e12127aed187e67013e77b6458e78" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch.rs", + "bytes": 34485, + "sha256": "a462eb627ae19348072775cf8e60b17d62f6b682b74cc413906d493fba1f0b3c" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch_tests.rs", + "bytes": 27546, + "sha256": "b67c4d7dcabac710cf3d30730a8db323180a558a415e2d117c868cdfa4804619" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose.rs", + "bytes": 27155, + "sha256": "c6adcc9882dde65e9f04f6eda7f0c855c5cbb285cf421123029169112793ce9b" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs", + "bytes": 9809, + "sha256": "440daf8d43278ef981c620d1818d0daaed8fb83193b8e8ff67b2e42fbf882938" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructor_tests.rs", + "bytes": 54132, + "sha256": "85149521ad7df62918f6f2fab6da2b3346c342ab4bce7100de3f6206c1e81096" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructors.rs", + "bytes": 12258, + "sha256": "0982163439b06a42d9af1643530364109282e63f05b323a35a76d8241f3caa32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/history.rs", + "bytes": 3211, + "sha256": "df038c2464eab8093a983f3104f0d04ca8f602010a50e1d3eeb8e53f2309e201" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed.rs", + "bytes": 14175, + "sha256": "bdd6c0fdc7108056bc5d7433254503449eed2209813680e717ead24b9d0ce834" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows.rs", + "bytes": 13222, + "sha256": "4b7bf25806471ad22d89253b9af8000f5acff53b4cc0aa1c1a83138e7b270eca" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs", + "bytes": 30804, + "sha256": "1a3b09e7ed100411cdf8043f9a78defc664a873ee0201117709185c201a5c57f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_tests.rs", + "bytes": 31163, + "sha256": "9dc9b989e5a9d408427ca1baf86b2454f9dd3baf40a551c70ca2359dc18a1227" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind.rs", + "bytes": 789, + "sha256": "4ed19952ea511d1b3226564a138e3322f994371d6b49794c14bc3689d1393e7f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind_tests.rs", + "bytes": 23424, + "sha256": "5fcf9930e949aebeb12eab0327264f32a2388d62dbf098bd2365711e22623c52" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered.rs", + "bytes": 9705, + "sha256": "6a1ed894d1b9d0110bc784a75c49444f1d14f1f74620d701f22203b2d8d0a813" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered_tests.rs", + "bytes": 22383, + "sha256": "5c8ea3a69e3da3b94d13f50f12aa7dc53246ccba25ffcace9552a0eb76b0bf36" + }, + { + "path": "crates/oa-desktop/src/semantic_review/recovery.rs", + "bytes": 28769, + "sha256": "88e69044d90c481593bab7cc125f0401c05202fccdde07698cec9483947fe8be" + }, + { + "path": "crates/oa-desktop/src/semantic_review/row.rs", + "bytes": 38159, + "sha256": "b08fdc503c6c4d2ff99adac7e18c9baa861bf86155e00b55d1e492430980d0f2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/scalar.rs", + "bytes": 15153, + "sha256": "9ad202174934dd9a63fbf9cba43135aead571db60846c8b7b1d3019a11df26c2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/source7_tests.rs", + "bytes": 11665, + "sha256": "5e8221d77b068fa30c1571988448cb8829711735881d810f3d333cbc95d34a9f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/tests.rs", + "bytes": 32480, + "sha256": "077ec6501dfaab0b05ea3987943b204f08352952296d313555051d998ccd232e" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view.rs", + "bytes": 57626, + "sha256": "7cc7dd213ab01dadad5debaee868c30a1099466602869a799ebdf9b680e8eaf0" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs", + "bytes": 4737, + "sha256": "6c766b29c8664d15cb113d2cc71bba9b60ae4ccf7e44c9c5d3c41e1dc4b7ed90" + }, + { + "path": "crates/oa-desktop/src/source_navigation.rs", + "bytes": 3938, + "sha256": "c1da10882f35f8eb0675e68c642c9a209dde09679cded414cfb9903a4e49ec77" + }, + { + "path": "crates/oa-desktop/src/source_tree.rs", + "bytes": 21816, + "sha256": "4d55e0110a1eb128a5d26c28f3aae5853d79b7245812b7c82bd275f38440a5aa" + }, + { + "path": "crates/oa-desktop/src/source_workspace.rs", + "bytes": 15522, + "sha256": "30f63a6b8d65a38d66061f90d864b5746cbacc0fbd65abc028a8f3071c47d358" + }, + { + "path": "crates/oa-desktop/src/style.rs", + "bytes": 3015, + "sha256": "05d61bec16141612ccafa0988e8c4827f41754509efdb3b961eab69127ded896" + }, + { + "path": "crates/oa-desktop/src/tests.rs", + "bytes": 58557, + "sha256": "b017218195eea2c96aba12eba8a99a38945c8ca910a652e272d34883bdb98691" + }, + { + "path": "crates/oa-desktop/src/view_copy.rs", + "bytes": 9043, + "sha256": "5630f31c942e1226aafd67eabdace6a5c5494875bdc6f7f077019e8110ba41a9" + }, + { + "path": "crates/oa-desktop/src/views.rs", + "bytes": 48347, + "sha256": "7148e01e8479c2294093159969bc33b9ff0cf916799d481681811c4de5c60e53" + }, + { + "path": "crates/oa-desktop/src/views/traceability.rs", + "bytes": 7501, + "sha256": "4c89dcf35c25e737d8cce4d543a5989ca104a5678e6cf40df47e457773d2ef94" + }, + { + "path": "crates/oa-desktop/src/workflow.rs", + "bytes": 82963, + "sha256": "d9e7b11a86fd3ec79d91f0c9bc9cebd5a2043cdaa7261622454fb7730ad8e060" + }, + { + "path": "crates/oa-desktop/src/workflow/communication.rs", + "bytes": 11084, + "sha256": "34d121ad97706012722e4bb6210072ecc3a5b4718b0ceab0c3d0f14d4cd780a1" + }, + { + "path": "crates/oa-desktop/src/workflow/recovery.rs", + "bytes": 9859, + "sha256": "0849b0f34dba5e8761be5fa05784d134ac74e4e9db395d6b9b7d6cff71e3f3dc" + }, + { + "path": "crates/oa-desktop/src/workflow/transition_tests.rs", + "bytes": 8553, + "sha256": "589b1b2dbadca74a52c2cfcfd4fe620bd4790abd113ac02182e91616c52d793c" + }, + { + "path": "crates/oa-server/Cargo.toml", + "bytes": 778, + "sha256": "527ff5e15010a8b560a6659aa1ae4bf0c8df0d9e019f03c47529ec6a750c0982" + }, + { + "path": "crates/oa-server/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-server/src/error.rs", + "bytes": 5610, + "sha256": "93817f1502386e4662c96ee1c7d1c4afc4e33bda6fd64e46c86c8e1afd5a9368" + }, + { + "path": "crates/oa-server/src/lib.rs", + "bytes": 7743, + "sha256": "20bef350350ed6339588b23af684abf745f97916c7edf3853ee042abe687e664" + }, + { + "path": "crates/oa-server/src/main.rs", + "bytes": 4163, + "sha256": "d59a6a2b5c7034bd2e00a516d2d49abb16391bf0316c1c357ddafb9c82d2d1e3" + }, + { + "path": "crates/oa-server/src/preparation_http_tests.rs", + "bytes": 5523, + "sha256": "454b90d0055d35644c8d990cd7576ddf675a9b271ca07fe9b0efce25d8ddbc07" + }, + { + "path": "crates/oa-server/src/routes.rs", + "bytes": 46053, + "sha256": "3f9a0c22043dbf28f7a8e587d2fc3abf869f76f4d23a46fe0aeb9c4392454a4b" + }, + { + "path": "crates/oa-server/src/store.rs", + "bytes": 25524, + "sha256": "37dcfb845b7993d5bced2bcf25fedfcd2a9c20a748f9315afe6cca1f41f89ae5" + }, + { + "path": "crates/oa-server/src/store_preparation_gate.rs", + "bytes": 4440, + "sha256": "427c390da3c0281ba1fe115a93ea90800e3b45fce6bcb9185e71982ddd885fec" + }, + { + "path": "crates/oa-server/tests/http.rs", + "bytes": 71892, + "sha256": "50daa6a90e96ad6fe3c6a27df2eaebe486270599c203892d09769c547a194975" + }, + { + "path": "crates/oa-server/tests/http/communication_limits.rs", + "bytes": 5790, + "sha256": "ba5474dd8e494a0176fffb4548c8e3deb2081b15de33bfce82394c764e0a2c16" + }, + { + "path": "crates/oa-server/tests/http/data_contracts.rs", + "bytes": 25949, + "sha256": "8cd456db83f301baca380acf025a5a99db4edf7d8839537dfbd02c4307e10e6b" + }, + { + "path": "crates/oa-server/tests/http/port_anchors.rs", + "bytes": 15086, + "sha256": "7199d0a208ddad9e1518b6e3fb390d64da111716d7505ba5c995deae19ad1258" + }, + { + "path": "crates/oa-server/tests/http/semantic_transitions.rs", + "bytes": 22832, + "sha256": "856d1c9b633ffb581e3e631f4e5a607789a02181dbf060667f208a569bfa4920" + }, + { + "path": "crates/oa-server/tests/http/source6_views.rs", + "bytes": 13023, + "sha256": "867bccda0e030eeeb9976b4ceeb47b21ab5044d700278022d5fb495bbb64ca07" + }, + { + "path": "crates/oa-server/tests/http/source7_contracts.rs", + "bytes": 11000, + "sha256": "0e0a467186da7263c0754890aff69ebd0d92d36b5224e4865bb6854b18dab7e8" + }, + { + "path": "crates/oa-server/tests/http/source_projects.rs", + "bytes": 30907, + "sha256": "8ff580f317ec267d1cd72ae14d8638f74a77cdf99ca4c78f66f4290c2d77460a" + }, + { + "path": "crates/oa-server/tests/semantic_http_contract.py", + "bytes": 7251, + "sha256": "4737ff94f10761f94151a694414c5c62737e0391ec820bca132830652bc4cca2" + }, + { + "path": "crates/oa-server/tests/semantic_schema.py", + "bytes": 25111, + "sha256": "cacca116c42ffbc097f1419c0f89921fa0dfd3faa864d46820ff744cfe6967f9" + }, + { + "path": "crates/oa-server/tests/state_representation.rs", + "bytes": 12241, + "sha256": "6686c826f28248ae4b63ec3f2112e193d4a347724b87974483217fcfee49cd61" + }, + { + "path": "docs/acceptance.md", + "bytes": 3463, + "sha256": "32ed3e4fed63f5c464e0937221f401d71623b276dd70f083531ac6ccebbf9050" + }, + { + "path": "docs/api.html", + "bytes": 72515, + "sha256": "a06126cb741da57e9c06e83fc4137ccde0de05562673546bc9d405878f51ea07" + }, + { + "path": "docs/api.rst", + "bytes": 41929, + "sha256": "ca7d310138c3b4d429d80a1d03d1ac8d4a9296069e205da573034256d10438a7" + }, + { + "path": "docs/architecture.md", + "bytes": 10974, + "sha256": "a50bd1645222ef3e574fa07a6ab9aa213431ee8833c6c0158ad91ce0f799543c" + }, + { + "path": "docs/language.html", + "bytes": 175111, + "sha256": "67dcfa3477b48a0b07298ec81741840cc3ccff12c7d58164873d746d41d611c8" + }, + { + "path": "docs/language.rst", + "bytes": 130753, + "sha256": "ac6b1d982c55f246de2f3c844b24b874599a3c20cfbd7ed22efe4561ac622b83" + }, + { + "path": "docs/linux-packaging.md", + "bytes": 8671, + "sha256": "41a1bb1095801df37a1411341c5791d55ea8c365739314d599a60269ac4bc7a0" + }, + { + "path": "docs/openapi.json", + "bytes": 346920, + "sha256": "bc5825a3182a4df5f6710a80262f4a09b4c15f0e4cda7ca74e1746ceca81a89b" + }, + { + "path": "docs/research-v2-architecture.md", + "bytes": 40445, + "sha256": "1b3809f2dabe6b1805dbf3d3f2625c471cd21356c7bd603916466a8c4ced8090" + }, + { + "path": "docs/research-v2-semantics.md", + "bytes": 31535, + "sha256": "b7ec3677300248926a88b84da3e7ffe52f5b73524c7712c927a0e951569db04f" + }, + { + "path": "docs/research-v2-service.md", + "bytes": 36209, + "sha256": "4828df5bc679e7c71f49b4b85c9e7dacbec52f46d416fed48af70e08886d4e4e" + }, + { + "path": "docs/research-v2-ui.md", + "bytes": 3245, + "sha256": "894e721b59a8ca85f0d95b6706ed5f130a0372def460cf33aeefc9e5e38cde0a" + }, + { + "path": "docs/scope.md", + "bytes": 16260, + "sha256": "58b88a7f0b1c8a79edf23c375a141d7b1b821bca4f3212a80e29db5ea3fba796" + }, + { + "path": "docs/server.md", + "bytes": 23361, + "sha256": "4eb24937d424ba8b384e1734e02a6f8e963ae11d2715889c92b14b39ff2e4a48" + }, + { + "path": "docs/source-cli.md", + "bytes": 3374, + "sha256": "34f12b72cc6d6ead4f47c7818c1897db592f8ad493c70776db2ab48c24d8f051" + }, + { + "path": "docs/v2-interface.md", + "bytes": 11807, + "sha256": "c7fbaf0ebd3fc9f6c11f818db2b0b9d68a33f580cfced22bde83ce08ae790bc7" + }, + { + "path": "docs/validation.md", + "bytes": 6505, + "sha256": "f46ea950ed1a54409aa07c4f9e90f39f8e2d00c1dd1b6cb805caa3993845967f" + }, + { + "path": "docs/windows-packaging.md", + "bytes": 10406, + "sha256": "2c852ff7041713ffbff3c951ac98df647c51fb70b0feba9000f7bfd5bc2703a8" + }, + { + "path": "examples/coastal-rescue.arcadia", + "bytes": 33107, + "sha256": "7e183ef9fd8154d3074303f1956cd0d9871c19e3ffc630d333b0bffbd6e30e17" + }, + { + "path": "examples/coastal-rescue.json", + "bytes": 49715, + "sha256": "242f0b031229a87af5c4ebe5d651defe59c191a6b7a69dc0a77f7e203411f195" + }, + { + "path": "examples/legacy-v1-coastal-rescue.arcadia", + "bytes": 3449, + "sha256": "4dbbbdf8bc7e6cb8e0a81dd678069dae585a9e5187c59bc3665e1fdab3f1a86a" + }, + { + "path": "examples/legacy-v1-coastal-rescue.oa.json", + "bytes": 5976, + "sha256": "fd0e9dedfb7adb922dc0575cc2b820d40344d639f1d8052bd70363aa59de9c24" + }, + { + "path": "examples/release1/README.md", + "bytes": 11137, + "sha256": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5" + }, + { + "path": "examples/release1/evidence/repair-check.md", + "bytes": 1419, + "sha256": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5" + }, + { + "path": "examples/release1/expected-diagnostics.json", + "bytes": 288, + "sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e" + }, + { + "path": "examples/release1/model.json", + "bytes": 134520, + "sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727" + }, + { + "path": "examples/release1/model.rs", + "bytes": 28762, + "sha256": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5" + }, + { + "path": "examples/release1/oa/activities/activity_oa_restore_work.arcadia", + "bytes": 1307, + "sha256": "83e73d0a013d5c3e1a4af0d9139ab0cfa362d25a846671fc2dac59181061a9d4" + }, + { + "path": "examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia", + "bytes": 697, + "sha256": "172c32c044ef4b5bc58a1f4635cc81f48dabb4014479b47dce7b25fedf2dbd0d" + }, + { + "path": "examples/release1/oa/behavior/process_oa_field_procedure.arcadia", + "bytes": 1781, + "sha256": "d2552dc5383e2e91cb302a790364e197f663c2edc013a2ab1978789825c9c324" + }, + { + "path": "examples/release1/oa/behavior/process_oa_response.arcadia", + "bytes": 3225, + "sha256": "cf86f46fa5c5d70592ef43f1bc1cb4d968efd8add384ba88036bef6b88aa4abc" + }, + { + "path": "examples/release1/oa/behavior/scenario_oa_conversation.arcadia", + "bytes": 1795, + "sha256": "b25be524e98a35ebac5e4481b7609c027b32baf9605e8097f807560f94a77ca6" + }, + { + "path": "examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia", + "bytes": 1261, + "sha256": "00a1edf2c9707a0eb62ac070c2d02467874f2c9c1f273cc6ddb53d6d65999a1c" + }, + { + "path": "examples/release1/oa/behavior/state_machine_oa_monitor.arcadia", + "bytes": 2022, + "sha256": "447c9ad1359f4a31ecf975ad07c0b08d8e7e03191fb0858e2fc56f377b223cd5" + }, + { + "path": "examples/release1/oa/capabilities.arcadia", + "bytes": 781, + "sha256": "fc8012e5554350b12e1cb80080f8feab80d933c9b33c0f2a7bb500cb7d609936" + }, + { + "path": "examples/release1/oa/model.arcadia", + "bytes": 661, + "sha256": "f7cf914766ea73ec53b85e32d117ea9a9dba52d3e038246863541ae7069b1a15" + }, + { + "path": "examples/release1/oa/participants.arcadia", + "bytes": 1834, + "sha256": "951764c14179f27c10e315acf2ea37d1fff54c77b8441e23addd239e8c715ce3" + }, + { + "path": "examples/release1/oa/types.arcadia", + "bytes": 788, + "sha256": "9d0396fb82dd5733edd8d1cf52f968ca84906927c704f9cf0944a7fae215cf49" + }, + { + "path": "examples/release1/project.arcadia", + "bytes": 1958, + "sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda" + }, + { + "path": "examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia", + "bytes": 706, + "sha256": "73928ced61c06cbc08728d7956676a50fd4c05aa5b7f1ff4f29ce27a0b0f2088" + }, + { + "path": "examples/release1/sa/behavior/process_sa_field_procedure.arcadia", + "bytes": 1790, + "sha256": "886d462af83110cbfa55dd232a667ea1446761234471357cbb878794e98e8768" + }, + { + "path": "examples/release1/sa/behavior/process_sa_response.arcadia", + "bytes": 3234, + "sha256": "041785fc22842423822431bd5e6e8d16094ebd0716965e7c765fb8c1d7549e8a" + }, + { + "path": "examples/release1/sa/behavior/scenario_sa_conversation.arcadia", + "bytes": 2127, + "sha256": "97276f5757ee9aa12a0dbd9d3ccd58174b3091162bb8bcce357cec20a45f5d85" + }, + { + "path": "examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia", + "bytes": 1256, + "sha256": "e12fc406b99d60a2f7768aaa0b25ba560e8605f0b8e37f3a7cdd3b8443562b74" + }, + { + "path": "examples/release1/sa/behavior/state_machine_sa_monitor.arcadia", + "bytes": 2022, + "sha256": "05ce29af99424c491c47fe2450dc6f5f8ce995511c01ec7e17bb5855be3c9736" + }, + { + "path": "examples/release1/sa/capabilities.arcadia", + "bytes": 781, + "sha256": "b6912fb3ece439912345fa71724acd462e716a1e5a88203a1058a5c3b2c20a4f" + }, + { + "path": "examples/release1/sa/functions/function_sa_audit_thresholds.arcadia", + "bytes": 297, + "sha256": "e5a04325d52183956dca814dd90f4e56c96c6b7db0f4f202f203ecb49c3e0f4f" + }, + { + "path": "examples/release1/sa/functions/function_sa_restore_work.arcadia", + "bytes": 2576, + "sha256": "fddeb67308051325041fee641fe758ddf43345b7c47b66d433616ad07fdccdd6" + }, + { + "path": "examples/release1/sa/model.arcadia", + "bytes": 661, + "sha256": "d431509251acb859fbb98733b6d18add952495e10a78703fbe8f31e40bc083c4" + }, + { + "path": "examples/release1/sa/participants.arcadia", + "bytes": 4012, + "sha256": "780d2eb08b217b67b55febb3d34846f727b8ee9018df6c5741748647e36b2ff8" + }, + { + "path": "examples/release1/sa/types.arcadia", + "bytes": 1227, + "sha256": "101fc0fbd99f1101514aa7b6da4f8e535aae0bb94c9e2b9e7e6c151135b28660" + }, + { + "path": "examples/release1/traceability.arcadia", + "bytes": 29506, + "sha256": "48a96f543456c9b5a5798fc795594e4119147fe45f11a6f109f141b7f514560f" + }, + { + "path": "examples/release1/transition-before.json", + "bytes": 35801, + "sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d" + }, + { + "path": "examples/release1/transition-request.json", + "bytes": 4276, + "sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a" + }, + { + "path": "examples/release1/views/diagram_oa_allocation_view.arcadia", + "bytes": 2176, + "sha256": "dfc0eb61e0e2aa5bda45524571fbfa3494f44ce19696f3e15e07438074a07bc7" + }, + { + "path": "examples/release1/views/diagram_sa_allocation_view.arcadia", + "bytes": 2173, + "sha256": "b78b43a26a82efb8fbd5374b09c329fa341f57b29f635d397b9ef8aede71d389" + }, + { + "path": "examples/release1/views/diagram_sa_capability_view.arcadia", + "bytes": 1620, + "sha256": "75e8dc9fb30f25d6f8fd3c6b595593ab4f98958a25798f961e336e0fac557649" + }, + { + "path": "examples/release1/views/diagram_sa_service_view.arcadia", + "bytes": 2209, + "sha256": "b08c84f3ea633e7c78dfdad3272f75415f6256cf3614b6d99bfd4f0269076c9b" + }, + { + "path": "examples/release1/views/diagram_sa_state_view.arcadia", + "bytes": 1931, + "sha256": "5f42f4c605e57b817d8400ed21f2b14bfc4e1592aebe941ff113767504c2b869" + }, + { + "path": "examples/routing-workshop.arcadia", + "bytes": 66431, + "sha256": "dc6b75da72884d399132ed9e300fa5a26b73d802bb526c9af986ae9eeb0a646a" + }, + { + "path": "examples/routing-workshop.json", + "bytes": 100281, + "sha256": "8592714f75b271133f21ce017d87896b6cf91ea0e4837f1c2ed453e21be26b14" + }, + { + "path": "examples/source-project/oa.arcadia", + "bytes": 2142, + "sha256": "5f60642f211b2d352a951092faeeb7503d4fb7f6f7fca99e810347231101d31c" + }, + { + "path": "examples/source-project/project.arcadia", + "bytes": 606, + "sha256": "787f6b67b179ffd221f7e5faf1a4edd74cfd116aa1e3b5e9ed9f22fbff612d8d" + }, + { + "path": "examples/source-project/sa/assurance.arcadia", + "bytes": 682, + "sha256": "ab76044836e4dcd42a82d316846ec87da2108939aa2037640c5bca1d2ecdee0f" + }, + { + "path": "examples/source-project/sa/participants.arcadia", + "bytes": 1231, + "sha256": "1abad579812e77877b45a3d8e3c3e9da734be2d04a0254cc730a5257ebd2dc88" + }, + { + "path": "examples/source-project/sa/reporting.arcadia", + "bytes": 2839, + "sha256": "f0b7443f728912ca163de39b6055876b533a77feaa3219b2d40a2dd65712e058" + }, + { + "path": "examples/source-project/views.arcadia", + "bytes": 3508, + "sha256": "e29ad3f406a6a50d2f0ae2ca2a53ee3b0c18222cd4098704a46d4f48b64ea17f" + }, + { + "path": "examples/text-first/README.md", + "bytes": 1367, + "sha256": "4ec8250d5ed556f1d496e8c85137e973e802ecb11df1a36488411e8cdc5a9f9e" + }, + { + "path": "examples/text-first/project.arcadia", + "bytes": 480, + "sha256": "f441535c623e0cf20e1875eb2cd33b587ed00e078c37af39cb36d89bb5edc19a" + }, + { + "path": "harness/fixtures/basic-editing/README.md", + "bytes": 12012, + "sha256": "3545d9695e7c62e0caa818efd7f4dc426f04876a3626e0f3b1bad606b258cb26" + }, + { + "path": "harness/fixtures/basic-editing/base/expected-baseline.json", + "bytes": 4524, + "sha256": "f239e728d650b0af32bb8f3dcaf6b27c99b9c5041132ac14e51756ea6563d978" + }, + { + "path": "harness/fixtures/basic-editing/base/project.arcadia", + "bytes": 3077, + "sha256": "045cb047d7bfbca72c3fb30db4c81a30ad463d3af7fc51604906ad7fa75a1ff8" + }, + { + "path": "harness/fixtures/basic-editing/base/rationale.arcadia", + "bytes": 57, + "sha256": "c01867c9dc0d40726853119fac3c6ad24a40d8d362a93a8b9d20df04ec2e273f" + }, + { + "path": "harness/fixtures/basic-editing/manifest.json", + "bytes": 447, + "sha256": "d23c21b02064ddb85e051e10e16ff089daba8d9910bb1a695dfabe2db3757734" + }, + { + "path": "harness/fixtures/behavior-creation/README.md", + "bytes": 5954, + "sha256": "9e5fc751424d12cfe76a84abb1da45b2d38947409179451bb90ffe7e7ddc8723" + }, + { + "path": "harness/fixtures/interface-operation-probe/README.md", + "bytes": 2604, + "sha256": "088f0bdb3375deb8f2c7e3b749ca8d210ea828ca4dece69b3871b674e3110175" + }, + { + "path": "harness/fixtures/interface-operation-probe/hashes.json", + "bytes": 977, + "sha256": "f6d3e8aa42f40f64ce2ecba7b9cf112eb9091f3cccde3a592b0434e834cf2a6a" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia", + "bytes": 4218, + "sha256": "c0fe9570684c0061efc4cad65e339fede6961b04ca90c6f324c787bd65e61226" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/after-reset-deletion.json", + "bytes": 7835, + "sha256": "93187f7a1b476a44185725c6b15f2e1767482b6911ab1280144348d033eb51f0" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/boundary-only.arcadia", + "bytes": 4771, + "sha256": "02f85e39957ebe6d56336fa729a5c8b10f51865face9d3b2c9679200571fcfc7" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/boundary-only.json", + "bytes": 8840, + "sha256": "756f9710336e6734ce3592ca774c6f104bba5a37c9e165540eb39326682393d1" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia", + "bytes": 8635, + "sha256": "ac5b629ba5596464f9f8a975b8bc2764e3007ceb7342de3f828d4b18e4f069b7" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json", + "bytes": 16160, + "sha256": "78a47c978dc667aa1c45af711f06aa2a0d3c581003013e30c3e02fb0faae2e64" + }, + { + "path": "harness/fixtures/interface-operation-probe/probe.rs", + "bytes": 11274, + "sha256": "72b1b443e25eb20420dec4477b0fb5ef5d1c705e3882c81df88dd578ab96a426" + }, + { + "path": "harness/fixtures/release1-native/README.md", + "bytes": 2564, + "sha256": "5f5ba040fd2df9261a29bcab3f9a7ae27c3683265411ad3dda0b467d5df8590e" + }, + { + "path": "harness/fixtures/release1-native/artifacts/README.md", + "bytes": 4155, + "sha256": "24200c84ccc2ef3e17c029744074e5cea785188f5549ac54e44cf89682643f81" + }, + { + "path": "harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs", + "bytes": 9275, + "sha256": "444f360b78df52c5e42046f77b148781e0c9bddc2a09c0b7bd65c7a4f376cfe6" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia", + "bytes": 7768, + "sha256": "84b22e95c213018612e6e8a84cad68b35a8b495ff3515b0f978c5e7a3d196d79" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json", + "bytes": 7330, + "sha256": "13a6f99ec2df1dd35ee8b2613f183918cd9537eda4a5da085ad52e65c033db59" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia", + "bytes": 1446, + "sha256": "1e0d662ba33071b122ad9206d2d559a8e748c9431eb3ac1b759bde3760ca877c" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json", + "bytes": 2216, + "sha256": "fa6a2e3643c0f909b92407abec3edc897033162eeaa501f8d4616f79d41fa044" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json", + "bytes": 567, + "sha256": "65cb70b0d697a055363ba01bf30bed7e412d43c9978a033549c378d117719ae7" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json", + "bytes": 3991, + "sha256": "ed905267263e7f8a0cd2dbe833196b30118b6a246815216932fae661ad650699" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia", + "bytes": 6837, + "sha256": "e50fc462ec22dbf98011929931dd2489430001c3e6433608685b9d88bceb9acb" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json", + "bytes": 6433, + "sha256": "f27a7b38d59c7cefe58cb36783d3d4dd8d8ae1168d9aed1eb61d343c18758908" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia", + "bytes": 1807, + "sha256": "eba2aec39e6efb405eace4bc8ab0c5563907c26d449e71a4aa750df9f73233b4" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json", + "bytes": 2673, + "sha256": "2ef777f19b94f08b5e5ca2ef727777897e1018f1426eee291408b3c69101f596" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json", + "bytes": 519, + "sha256": "3af7232ed3056ca84dcaf0f266ead3faaa512a0dc0051c7ba68791c20dbae0cf" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json", + "bytes": 1246, + "sha256": "8af21b00b62e620256fbf50e47c6c5836cdb6922d9549cc4b3d86bc03b008073" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia", + "bytes": 4200, + "sha256": "20bb30473830e44228958e78fb7480269cdcc0580c6c90c718da75f34ca5a43f" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json", + "bytes": 4552, + "sha256": "44f4b05db49758dcf344788f57cecc0250fd029099b1c54e4e1e3e972f6cf039" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia", + "bytes": 1808, + "sha256": "b0fd031671c9f755756922ec5c40a0ce3f40164d22c3c389aec9fd0705344da7" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json", + "bytes": 2673, + "sha256": "1acf81e08c51c85aceb14a3efbc39ec680552a19c7824aab4611ac3cf71b9c6d" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json", + "bytes": 610, + "sha256": "a07978f4b0a3ca83e5ae5f35e7b66b26473b24431fc90e5889d755f1816d52dd" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json", + "bytes": 2352, + "sha256": "5ff844a038b0030adb5a83d13d81e60f30627543388f6199b6a0e026fc42ea58" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md", + "bytes": 4075, + "sha256": "a51ffbc45b6c172cbfed3b16c249296e6a28d457fea57393dd958cdcfbafbca1" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json", + "bytes": 2367, + "sha256": "3ad8b7428b3ed7585a91e7f5d9d5f4880caccfd0b08dca25d05af523e62e0a10" + }, + { + "path": "harness/fixtures/release1-native/communication-means/README.md", + "bytes": 15091, + "sha256": "015c16010c212af283f9def118c6d263ba65a3bc6e81a750f180624ca69bb311" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia", + "bytes": 6098, + "sha256": "a6976e7c21031e7cc3cb731e20c3075cf89cf85144dc8d3f52930f8521ea196f" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json", + "bytes": 10480, + "sha256": "60c4419415c546f06c9972ac7bb0e530ada7b30a200b59e75ddda231a24f9a3d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia", + "bytes": 407, + "sha256": "3c41459f2b0706a767988738bf8126f76540415bb5940d9c5db19cbe8c18182d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.request.json", + "bytes": 1331, + "sha256": "b16e673b29c96b5aed681389ff7669110abe552939e86dd0ec6011c9a45ed860" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-draw.arcadia", + "bytes": 5239, + "sha256": "a511a4d817d9c5c80233bce83e84a7056b7dce7afd9fcc25696e333f8f4df055" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-draw.json", + "bytes": 8424, + "sha256": "7140114926228b1c378c450535f7a70992c71a879a8d2270c49e8d5f04a2f484" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia", + "bytes": 5710, + "sha256": "910e6dfe609467047ce629ec9c626f4e927cada74bfbfa164414a8fe6783ea87" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json", + "bytes": 9868, + "sha256": "567259e20c9f6278776e8fa99b8b2fdbbc9a883ad875c5d39dae1b53de3a2924" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia", + "bytes": 407, + "sha256": "997f72fe9258012df66e88344cf39c1a2cebf2327f604056b67f7f2a8ebcfbfa" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.request.json", + "bytes": 1127, + "sha256": "cb3fe05859cfffec6f8846c19983dc462413bc0ea4f6e19204f09c259919c4b1" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia", + "bytes": 8209, + "sha256": "ec826b9b84616507490ac30e4d57100dd6338dba2f7071e879a8fb58820a85c5" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json", + "bytes": 19735, + "sha256": "ef07e4d2d3fc36004ff01739838281581d9a703c747611eacc01bb9cc672489f" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia", + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json", + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia", + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia", + "bytes": 9827, + "sha256": "86dd8a355f6c9dadfea56d06e5247c9b0f820819d7df17ef5690671c7c81324b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json", + "bytes": 21877, + "sha256": "980489ec2c483d4f9efada61358c1f29bafb2572fd5b804c656b91b8e9bb4f9a" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia", + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia", + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia", + "bytes": 7698, + "sha256": "1abc4c4cac37cfb06ed3b12a115f9e7e6a2d3affd34b25c355309e5ee465fa40" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json", + "bytes": 13130, + "sha256": "a5cf4cb2ef390113dd752f4054d43fb789da2ca77799efb5782805ae8aa2dfd5" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json", + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia", + "bytes": 5454, + "sha256": "b761c4c2af5f7064aaf1f887ffd6094eb6ae834b2c8057ecdbc4aa97c042066b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-transition-ready.json", + "bytes": 8852, + "sha256": "2536b000de361fb131787e32b466b9123d4e98abf00c099ad32f6cb8e7fb530b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/generate-communication-means.rs", + "bytes": 28035, + "sha256": "003521404b702332082e01f2be7676fda1dd7b5bcf220bb81d25120c8984c75d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/hashes.json", + "bytes": 3133, + "sha256": "39b4dc02fc8b074015c59354fd1c1061c25832fde25065c7151f36eb0455e6f6" + }, + { + "path": "harness/fixtures/release1-native/communication-means/manifest.json", + "bytes": 1237, + "sha256": "fb921f42f2c4a4f8a67d1266f17f8c7750b6a2619554b713aa92b17918f0c43e" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40-oracle.json", + "bytes": 26264, + "sha256": "74edca148583f42c269e51f135662f65ef1c9ea1c94516d63e2a5091967dc99b" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40.arcadia", + "bytes": 28947, + "sha256": "00b87736b82c5afd33b311df7aebe9a6c8c51b6de66221181b5ea8bc8ca221a3" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40.json", + "bytes": 54938, + "sha256": "5dd4f22453330cab3abc059881660190e689a22619853f77cf6d0b52735d97ad" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments-native.md", + "bytes": 2624, + "sha256": "28906924b05f75bbf432389517aef65f1df474d2f2bff79a3586564f7f7328ef" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.after.arcadia", + "bytes": 6795, + "sha256": "9d084dd75a0251d349cc31f9ee98df58d0bc76859dc3cb1503f242e2f759888a" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.after.json", + "bytes": 9618, + "sha256": "f09be41afd85c18fd52fdf997e9b81c4b7cb2c41e78d167abcd14caea8110333" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.arcadia", + "bytes": 4990, + "sha256": "8740f27be99624013d9be876639c0411e1b8c14f0f9a843f43dc71d769efd79a" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.hashes.json", + "bytes": 963, + "sha256": "00fc9e2c709bdcd9ee98012176cb0edb635c112153d963cd169bf4bf69a9e010" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.json", + "bytes": 8146, + "sha256": "9c188f2890630c7feec2be719a7104af9690da7ea836df18fa78da3ff2acfc1b" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.manifest.json", + "bytes": 1045, + "sha256": "46b40f221ffad614733b615208814ace63947e96b80bbfdec63853ccd4f4e8c8" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.request.json", + "bytes": 3075, + "sha256": "b57f656f0b9cf2c595e83150c683301909e20f15684d35d108d3c57ee0dc5909" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch-native.md", + "bytes": 2637, + "sha256": "dddca2b85f892b05881c75ef3cd62437396cc086a09909b621e02bad0cbe56ff" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.after.arcadia", + "bytes": 3134, + "sha256": "d3315bba762395205ae761e6e64029e8b4070c15a1299c6114075581db7b0974" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.after.json", + "bytes": 3927, + "sha256": "8f36119c7e893a4af374b13b85b1c2cb3df0196df8d06a551386628fc8b8646d" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.arcadia", + "bytes": 1787, + "sha256": "5e1f433e04621424edac43e53411624059e873f88dd531bfdb39093953485ed7" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.hashes.json", + "bytes": 933, + "sha256": "1bd87700b04e4f169ab5f8aceaa3b0c3ec3a1cc43e1bce135a1cee506fc75d38" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.json", + "bytes": 2816, + "sha256": "1972242d2a8677d004dc3af1081feb292de65fe26128d877dded8661f9906619" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.manifest.json", + "bytes": 647, + "sha256": "1e440efa02e2ccc27585d9a49c7864d53e3e3e7645ccb219f040e00d95331b8e" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.request.json", + "bytes": 2197, + "sha256": "71f23474614aab90398ce24f00f2cf61d6cb9a3a34faa3e12e0c8104e0a71073" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-repair.arcadia", + "bytes": 15635, + "sha256": "e3ce0347746584f503b825a8b6bd23a7eebf0827940112ef8f36d500c1d0a043" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-repair.json", + "bytes": 25736, + "sha256": "29736f2a619f7f2d0e795229f6e8c3645a1b4eca6292043cd604aa01b1ec9170" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-take.arcadia", + "bytes": 15631, + "sha256": "9ab573c92be66e3bae659dd826923ba6066979b1651edda7bd469056632cd77e" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-take.json", + "bytes": 25736, + "sha256": "828c990c6129692cd31c7f6ed0dd6c952f9cfffdd90bc80171df82d4d929afba" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.arcadia", + "bytes": 15617, + "sha256": "49214be2ca9c8bb6195c3b39737c053847021839f70f0bb2921d35a1fbd2ac83" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.invalid-request.json", + "bytes": 2225, + "sha256": "8b02d4739b1be2c8a64da76423b4a6cae7d89c60f4d30723b94f1da3779cbcca" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.invalid.txt", + "bytes": 78, + "sha256": "aa536e18bc65325163e9b645a4ad031ab599fcb05948dcf741b725d0f0c8b602" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.json", + "bytes": 25722, + "sha256": "995dd72f5c1f07f998233ae9847e337cb82de0c2a9053d3905be021d5f8124b9" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.manifest.json", + "bytes": 1059, + "sha256": "ad315359bfd68ce0d265976dc9e4ea6a502ebbb87d7339dd1af5ef27dd01e433" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.repair-plan.json", + "bytes": 15016, + "sha256": "27bf316ad66ffa91f6323e75b0c280baf5caec298645793dea72c5d42aa3b782" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.repair-request.json", + "bytes": 2225, + "sha256": "f4f6b5480ec8927033ea469ea82c1e8411cb6dba95d2da680f15bc193914c9b4" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.steps.md", + "bytes": 3897, + "sha256": "7d7e38d93641a5f22a2738baa4dbacc392eebb477a855cdce0641961980a19ad" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.take-plan.json", + "bytes": 18557, + "sha256": "74dd68ba686b8928054480f0bfed55aad91f6f1208803399dc9878e894f57ecb" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.take-request.json", + "bytes": 1508, + "sha256": "d19a53002a6bf95af3123a3ef9f78baaf7ce1085d4f06f0f4a9d5e5d5accbc0e" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.arcadia", + "bytes": 6935, + "sha256": "90277f1780d43a6f0b2184bf00b87e7a4f3518d67c6f10d4793daafe1aabf493" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.json", + "bytes": 11286, + "sha256": "5a3c8494f6976c23b990c14b40760fb2f32ca9973caf6f31dab5632310663591" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous-request.json", + "bytes": 1379, + "sha256": "6a86fde194faa55f2d1bdb982571e0e711e5401b53a960e6acb8cf3e17f506c8" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.arcadia", + "bytes": 6195, + "sha256": "e7c30c5a5c3ba7209c8e223f02da55821d7af10a2f09844a7065ae6cd8f22aa5" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-plan.json", + "bytes": 19156, + "sha256": "d1e5c2ed08e0aa6274a0421a39876d1ba99f4e441c7a0a68660a83b9e017da14" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-request.json", + "bytes": 2418, + "sha256": "433f723252598994dac7184da61f95344b528156cc5a0fba1a7864b2ec9f002b" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.json", + "bytes": 10441, + "sha256": "9e4e2ce1c7879935ad0860a3ae720506c929a8422b51491b719d88af3fd3ff2d" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.manifest.json", + "bytes": 1042, + "sha256": "87e8899c7d4e54faedeef32afb7592fb65278d1bd7390850ab198dcf62068329" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.arcadia", + "bytes": 6280, + "sha256": "6c77ed42bbf9f4ebe08bfa233197a4020e5587ce33747f7914430aa0c662d9a3" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.json", + "bytes": 10009, + "sha256": "1dd7ca2bcfc84ac23369c506302b39569d2ebc41e5a4edc99378a1fe01923b7e" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous-request.json", + "bytes": 1390, + "sha256": "e0ea7aeb25687ae63c65128f1aab78273f14030d179d3fbea63fe91ab5430136" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.arcadia", + "bytes": 5529, + "sha256": "563c36162050c8564ffa25a85c726462ecdaebdb87659b3f3610506bfd5581e6" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.final-plan.json", + "bytes": 18821, + "sha256": "f96c43fc58dbe48a044b628b31bb3f0b59171dad5b6982ec6c34a5597e474130" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.final-request.json", + "bytes": 2438, + "sha256": "d38fdb13612b07254edad80848f79c1e097d5f868c020feae9ba072aa697a50d" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.json", + "bytes": 9153, + "sha256": "2ed0daed118f2df13dcad738dab174ad34d44a15921cf8097415e707978b0968" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.manifest.json", + "bytes": 1058, + "sha256": "c79bd12a85d8d4f25cc3ee43581b3e5e74ae9ad7e4de0c72764975ba2b89c279" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.arcadia", + "bytes": 5026, + "sha256": "98020e048ac01f285c6ad8c10ce22bef05e193b5ba0f5558be1fb26a7bf82620" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.json", + "bytes": 7851, + "sha256": "ea3783992488bc6ee944eb91a26ccf3dbc05a3909528fc32ec8023a76765cee6" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous-request.json", + "bytes": 1379, + "sha256": "ea3cfd25f7a50481805956fb88ab851516dcd602dd6564502a0cc55f7de6a54f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.arcadia", + "bytes": 4295, + "sha256": "2b7c3694623bbea3e36dcc6f674d4adcd91e23c8e1005ded21e5a69648a048e5" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.final-plan.json", + "bytes": 17912, + "sha256": "95a23bd334cffcaef0c3b1012b7555efd2a077ef6f9140d58020910ef8a76723" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.final-request.json", + "bytes": 2081, + "sha256": "1e2433ed5821bf65443f56cb27381ee935db81b1955d3a539b7952898312a410" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.json", + "bytes": 7012, + "sha256": "0033f8780c2ba69779b811bca792340c25a092f887e5811c6839f0a691d744f2" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.manifest.json", + "bytes": 1046, + "sha256": "e3b4a60a56abee779ca8356605c968aa2d99256a3bd3c5601b14b5c5502a1a05" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.arcadia", + "bytes": 5467, + "sha256": "e265cab35097acc46f5123107c13bc35b9f71931ff650557fc1c85779141b327" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.json", + "bytes": 8482, + "sha256": "9b777828d3681138de6bbd08de8bf922bed92c27be60b103d8ff73ce7ddd0a48" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous-request.json", + "bytes": 1379, + "sha256": "7d124256730bf2feee772c7d0118b87eb288e8aaa9179d69ceb9259dbceb2c71" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.arcadia", + "bytes": 4727, + "sha256": "87a5a3665dab2dd6e595867dcebb1586e93dd067c85d6e24b1a4d3ff0ad6d4ba" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.final-plan.json", + "bytes": 18803, + "sha256": "1888ca51e0923866a9afea99cad069fe5d3f19f0a1b24328e20ac1b9f52d8bad" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.final-request.json", + "bytes": 2294, + "sha256": "5070a5b85feb1bc7b1fc648cd5d35ef26663492c8666dcd471950c06ec3a6052" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.json", + "bytes": 7641, + "sha256": "725b35e36640c81c70c7efea5c3304bf61b49ef9664194ca0d491782cc62191a" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.manifest.json", + "bytes": 1038, + "sha256": "1b43abf8fed3061c65a7b7cb103d31ea7ece333a2da83c645f8d228448c4dbb2" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.after-order.arcadia", + "bytes": 5019, + "sha256": "65a6217f474b5085f37b511151185c2c08ff7a1998e924f1fe01f29c410f6ced" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.after-order.json", + "bytes": 7835, + "sha256": "8a53bb9b43eab5b6dca5e08deef20c14c96e134b9ee816a99b7960bb5255e008" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.ambiguous-request.json", + "bytes": 1379, + "sha256": "c704f99245f00d5c3be0562d91931b4e7f1fb3fb36f41991eb76656ffbd6129f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.arcadia", + "bytes": 4279, + "sha256": "c59ec1233d5a9fbb8e386f6d9b13f398be21baf813ba215efb51bfc1bb98db36" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.final-plan.json", + "bytes": 17938, + "sha256": "30c5c3f614a74708763df612016483623792f4c65e2ed2fa662ac417cfb852eb" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.final-request.json", + "bytes": 2098, + "sha256": "3b6c35ef78133f0980cb6fb53fbc29c5d3c4deb5dc4be01c2c3384e221987b9b" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.json", + "bytes": 6996, + "sha256": "c159c60403abcf8fefab7f19cbad9989488e8ccc20b82c57597b22a0ae254802" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.manifest.json", + "bytes": 1038, + "sha256": "af8a314a9b1d85a8f98affa5a6649f43d252851158798e1ad687bb1b6966b14f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.steps.md", + "bytes": 4549, + "sha256": "6004894e9d9f478b7fe1c3c3e0706dcc546d5df8557cff91b4cf26f8a4badf24" + }, + { + "path": "harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json", + "bytes": 1933, + "sha256": "71cca2a59f36b2a07d11dedbe9101226aa1a4a00d8ac244b5565202e56709f51" + }, + { + "path": "harness/fixtures/release1-native/source5-recovery/generate.py", + "bytes": 1496, + "sha256": "e44986c1d563499f25917441220b23883fc9cb0f89faf409873191f0212fbd2f" + }, + { + "path": "harness/fixtures/release1-native/state-actions.arcadia", + "bytes": 6224, + "sha256": "9cde82608110e56aa9e14c9e33eb38976f53aed74ab013145d411d5743a0ec26" + }, + { + "path": "harness/fixtures/rest-coverage/README.md", + "bytes": 1870, + "sha256": "df4a391312e0c8b20ccdcc850199443b08547be1b10b1f2447a067bf4bf544e6" + }, + { + "path": "harness/fixtures/rest-coverage/coverage-probe.json", + "bytes": 7951, + "sha256": "2d1ef7bdb5d7bf17b9fda33b1d1790d1d88289b3fd1e725f89b83a69771f9a43" + }, + { + "path": "harness/fixtures/rest-coverage/run-coverage-probe.ps1", + "bytes": 9381, + "sha256": "f24f19fca418b76a7249894a08c116e16b465a02f8f899d12b808ccfdd90a3bd" + }, + { + "path": "harness/fixtures/run-timed-bundle-oracle.ps1", + "bytes": 7466, + "sha256": "fab696486a574f9867c6e69a8a8b960e7e9722206441b9cc5f36d41f285f1a25" + }, + { + "path": "harness/fixtures/scenario-authoring/README.md", + "bytes": 14700, + "sha256": "6b651b180b76d10b00bc49d77a8f5e0b3ba0317446978f41b541f5bb23fedad8" + }, + { + "path": "harness/fixtures/scenario-authoring/base/expected-baseline.json", + "bytes": 38589, + "sha256": "abd8246c939ebb8a2162ef82e23d0fb322ee1a8b4d0d7c28a34037e42e86e841" + }, + { + "path": "harness/fixtures/scenario-authoring/base/project.arcadia", + "bytes": 19470, + "sha256": "9b1afbe96b0bf8fa613451b00929492f05a59b9eb2ca74c2c374a8b7bb7cba9e" + }, + { + "path": "harness/fixtures/scenario-authoring/base/rationale.arcadia", + "bytes": 86, + "sha256": "82df210f22f6aa336a3e7e00240977d671c7c3923db7bc58fb29e6d98b67ef49" + }, + { + "path": "harness/fixtures/source7-native/README.md", + "bytes": 7235, + "sha256": "7abccbc3da0980d1d099761d1a66d12c403a0f543954134537ecd4930d182b4b" + }, + { + "path": "harness/fixtures/source7-native/base/expected-baseline.json", + "bytes": 26428, + "sha256": "e2882a58f841f460f45d9d67db1606440e0d7997b9b29a784569ae48e4b23bfb" + }, + { + "path": "harness/fixtures/source7-native/base/project.arcadia", + "bytes": 13616, + "sha256": "5474d8ca05fd1375220dc0ad40d913631fde51159e9632bb9f290d1ecdab8dd7" + }, + { + "path": "harness/fixtures/source7-native/base/rationale.arcadia", + "bytes": 59, + "sha256": "e6a600e13036d4ad594a65380ed1b3d82d1772d583041bb0a624c9588a73047a" + }, + { + "path": "harness/fixtures/source7-native/manifest.json", + "bytes": 410, + "sha256": "6c57164a796234d83818d462d0a25f10e77062d405769e602ae2a7bec251e3b6" + }, + { + "path": "harness/fixtures/staged-source-authoring/README.md", + "bytes": 6876, + "sha256": "502be5e3bf9112447dc07b1a51fe0fa301754b47128a9b81877c185c6e797fee" + }, + { + "path": "harness/fixtures/staged-source-authoring/base/notes.arcadia", + "bytes": 63, + "sha256": "ad9de02963c20d1f43856455a797deb05ca674659e45f2c4b96b52783024f8bd" + }, + { + "path": "harness/fixtures/staged-source-authoring/base/project.arcadia", + "bytes": 593, + "sha256": "aa844950ec4fb86f262a257e1fc370a26557ea7978d8a4cab78265037660234c" + }, + { + "path": "rust-toolchain.toml", + "bytes": 66, + "sha256": "a6a0bbd29ffaa8182dc22d1d9149709f1091e47df40ed96eb8a78a711c66a4ce" + } + ] +} diff --git a/harness/experiments/canvas-union-windows-20260911/full-ff01754.log b/harness/experiments/canvas-union-windows-20260911/full-ff01754.log new file mode 100644 index 00000000..328301ee --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/full-ff01754.log @@ -0,0 +1,737 @@ + Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop) + Finished `test` profile [unoptimized] target(s) in 23.89s + Running unittests src\main.rs (target\debug\deps\arcadia_studio-e358a5ed59b1de41.exe) + +running 729 tests +test app::architecture_authoring::tests::architecture_allocated_work_is_root_owned_and_one_exact_history_change ... ok +test app::architecture_authoring::tests::architecture_blank_names_and_escape_never_accept_placeholder_or_rename ... ok +test app::architecture_authoring::tests::architecture_captured_escape_does_not_cancel_other_controls_or_placement ... ok +test app::architecture_authoring::tests::architecture_f2_routes_selected_node_to_inline_rename_and_exact_undo ... ok +test app::architecture_authoring::tests::architecture_first_focused_input_escape_cancels_create_and_rename ... ok +test app::architecture_authoring::tests::architecture_invalid_click_cannot_poison_inline_or_checkpoint_geometry ... ok +test app::architecture_authoring::tests::architecture_stale_submit_keeps_create_and_rename_visible_and_exact ... ok +test app::architecture_authoring_tests::architecture_cancelled_gesture_cannot_commit_a_later_named_draft ... ok +test app::architecture_authoring_tests::architecture_canvas_left_click_uses_world_coordinate_once_at_zoom_and_pan ... ok +test app::architecture_authoring_tests::architecture_checkpoint_retains_exact_name_point_and_fresh_binding_without_commands ... ok +test app::architecture_authoring_tests::architecture_context_choices_are_small_and_system_is_singleton ... ok +test app::architecture_authoring_tests::architecture_contextual_allocation_creates_work_relation_and_node_in_one_undo ... ok +test app::architecture_authoring_tests::architecture_creation_names_before_commit_preserves_camera_and_undoes_once ... ok +test app::architecture_authoring_tests::architecture_existing_inventory_is_contextual_and_placement_never_implies_allocation ... ok +test app::architecture_authoring_tests::architecture_f2_rename_preserves_identity_placement_and_atomic_history ... ok +test app::architecture_authoring_tests::architecture_name_survives_rejected_click_resize_navigation_and_escape_is_explicit ... ok +test app::architecture_authoring_tests::architecture_rejected_submission_keeps_inline_name_visible_and_invalid_click_is_ignored ... ok +test app::architecture_authoring_tests::basic_transition_explicit_overview_focus_wins_over_an_already_populated_architecture ... ok +test app::behavior_lifecycle_tests::incomplete_behavior_form_is_dirty_close_save_retains_it_and_cancel_allows_switching ... ok +test app::behavior_lifecycle_tests::passive_behavior_canvas_events_do_not_apply_property_drafts ... ok +test app::behavior_lifecycle_tests::pending_behavior_blocks_navigation_property_edits_and_model_history ... ok +test app::behavior_lifecycle_tests::pending_behavior_blocks_source_creation_and_source_history_replay ... ok +test app::behavior_lifecycle_tests::save_prepares_behavior_before_opening_destination_and_cancel_keeps_one_undo ... ok +test app::behavior_lifecycle_tests::source_writeback_failure_keeps_typed_behavior_form_for_repair_and_one_undo ... ok +test app::checkpoints::tests::checkpoint_completion_releases_the_writer_while_a_context_menu_is_open ... ok +test app::checkpoints::tests::checkpoint_inline_create_and_rename_restore_focus_without_changing_drafts_or_history ... ok +test app::checkpoints::tests::checkpoint_noninline_contexts_keep_other_focus_and_still_lint ... ok +test app::checkpoints::tests::discard_and_close_waits_for_the_late_checkpoint_then_retires_it ... ok +test app::checkpoints::tests::home_resume_keeps_state_view_while_explicit_architecture_remains_reachable ... ok +test app::checkpoints::tests::home_resume_retains_invalid_source_and_property_drafts ... ok +test app::checkpoints::tests::home_resume_returns_to_each_actual_workspace_without_model_edits ... ok +test app::checkpoints::tests::malformed_checkpoint_does_not_replace_the_current_model ... ok +test app::checkpoints::tests::recent_and_recovery_navigation_respect_unsaved_decision ... ok +test app::checkpoints::tests::restored_copy_keeps_raw_property_and_requires_save_without_local_or_hosted_binding ... ok +test app::checkpoints::tests::saving_an_older_snapshot_keeps_recovery_for_newer_unsaved_work ... ok +test app::checkpoints::tests::stale_write_completion_cannot_recover_a_discarded_document ... ok +test app::checkpoints::tests::walkthrough_request_keeps_unfinished_source_behind_the_normal_pending_guard ... ok +test app::data_paging_lifecycle_tests::paging_preserves_same_context_and_resets_on_layer_or_document_replacement ... ok +test app::diagram_selection_tests::changing_visible_view_follows_its_clean_inspector_without_a_model_edit ... ok +test app::diagram_selection_tests::diagram_switch_preserves_an_independent_semantic_inspection ... ok +test app::diagram_selection_tests::diagram_uuid_edit_follows_the_same_view_or_a_different_picker_target ... ok +test app::diagram_selection_tests::invalid_view_draft_blocks_the_switch_and_retains_raw_input ... ok +test app::diagram_selection_tests::stale_diagram_choice_never_applies_an_unrelated_valid_draft ... ok +test app::diagram_selection_tests::valid_pending_view_edit_applies_to_old_view_then_inspects_the_new_view ... ok +test app::direct_scenario_tests::direct_scenario_creation_and_reorder_preserve_selection_and_each_undo_once ... ok +test app::direct_scenario_tests::direct_scenario_rejects_every_stale_context_and_preserves_property_drafts ... ok +test app::direct_scenario_tests::direct_scenario_scroll_and_real_resize_invalidate_only_pointer_context ... ok +test app::direct_scenario_tests::direct_scenario_source_failure_and_navigation_keep_form_until_success ... ok +test app::hosted::tests::hosted_actual_app_tasks_publish_edit_conflict_correct_credentials_reload_and_open ... ok +test app::hosted::tests::hosted_catalog_invalidation_follows_service_writes_and_observed_revision_changes ... ok +test app::hosted::tests::hosted_catalog_search_credentials_and_failed_refresh_never_mix_result_scopes ... ok +test app::hosted::tests::hosted_changed_bound_head_does_not_invalidate_another_services_empty_filter ... ok +test app::hosted::tests::hosted_conflict_fetches_review_without_adoption_and_reload_is_undoable ... ok +test app::hosted::tests::hosted_detached_drafts_and_recovery_remain_available_after_rejected_publish ... ok +test app::hosted::tests::hosted_failed_cross_service_publication_checks_and_reloads_its_own_resource ... ok +test app::hosted::tests::hosted_failed_or_cancelled_local_open_keeps_the_existing_document_and_destination ... ok +test app::hosted::tests::hosted_invalid_and_detached_source_drafts_never_start_network_writes ... ok +test app::hosted::tests::hosted_invalid_local_open_repair_saves_locally_and_never_replaces_old_resource ... ok +test app::hosted::tests::hosted_modal_blocks_underlying_edits_and_credentials_are_redacted ... ok +test app::hosted::tests::hosted_new_revision_invalidates_filtered_catalog_even_without_a_displayed_row ... ok +test app::hosted::tests::hosted_only_successful_source_save_as_changes_the_active_destination ... ok +test app::hosted::tests::hosted_open_waits_for_unsaved_guard_and_failed_open_preserves_current_document ... ok +test app::hosted::tests::hosted_publish_acknowledges_only_the_sent_snapshot_and_retains_undo ... ok +test app::hosted::tests::hosted_stale_completions_do_not_clear_current_busy_or_adopt_another_document ... ok +test app::hosted::tests::hosted_unknown_save_is_acknowledged_by_exact_readback_without_retry ... ok +test app::lint_tests::committed_check_cache_follows_edit_and_undo_while_drafts_stay_separate ... ok +test app::lint_tests::lint_coalesces_work_and_rejects_obsolete_results ... ok +test app::lint_tests::prior_document_lint_cannot_enter_reopened_model_or_block_next_checks ... ok +test app::port_view_tests::catalog_creation_follows_the_new_port_and_keeps_group_browsing_explicit ... ok +test app::port_view_tests::filtered_active_view_cannot_hide_a_created_port_when_eligible_owner_view_is_folded ... ok +test app::process_canvas::direct_process_dirty_source_blocks_canvas_creation_without_losing_text ... ok +test app::process_canvas::tests::direct_process_app_open_source_and_back_preserve_local_reference_identity ... ok +test app::process_canvas::tests::direct_process_app_same_owner_selection_preserves_tool_but_owner_change_resets_it ... ok +test app::process_canvas::tests::direct_process_app_save_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin ... ok +test app::process_canvas::tests::direct_process_app_source_failure_retains_form_then_commits_once_and_selects_occurrence ... ok +test app::process_canvas::tests::direct_process_app_stale_context_dirty_source_and_properties_never_mutate ... ok +test app::process_canvas::tests::direct_process_append_empty_first_second_exchange_has_exact_source_history ... ok +test app::process_canvas::tests::process_focus_retains_rejected_draft_and_guards_selection_until_panels_restore ... ok +test app::reference_authoring::tests::chooser_rejects_old_results_after_context_recovery_reload_or_modal_and_keeps_history_text_out ... ok +test app::reference_authoring::tests::inspector_reference_browse_is_read_only_then_one_scoped_value_applies_with_exact_undo_redo ... ok +test app::reference_authoring::tests::inspector_reference_retains_invalid_missing_and_incompatible_current_until_explicit_clear ... ok +test app::reference_authoring::tests::mounted::mounted_delayed_return_cannot_focus_under_review_and_fresh_modal_tab_owns_its_form ... ok +test app::reference_authoring::tests::mounted::mounted_form_tab_enters_in_both_directions_and_cycles_boundaries_without_losing_focus ... ok +test app::reference_authoring::tests::mounted::mounted_fresh_tab_reveals_offscreen_reference_and_return_reveals_it_again ... ok +test app::reference_authoring::tests::mounted::mounted_reference_choice_stages_literal_identity_and_delayed_focus_cannot_hit_replacement_field ... ok +test app::reference_authoring::tests::mounted::mounted_reference_trigger_tabs_to_root_popup_search_escape_and_exact_adjacent_fields ... ok +test app::reference_authoring::tests::mounted::mounted_reverse_review_required_rationale_remains_editable_beside_candidate_reference_form ... ok +test app::reference_authoring::tests::mounted::mounted_review_pointer_open_and_outside_dismissal_keep_background_input_inactive ... ok +test app::reference_authoring::tests::mounted::mounted_scrolled_reference_anchor_uses_window_geometry_for_pointer_and_keyboard ... ok +test app::reference_authoring::tests::reference_array_row_reorder_and_constructor_switch_invalidate_exact_old_field ... ok +test app::reference_authoring::tests::review_reference_cannot_reopen_or_write_a_banked_row_after_another_row_takes_focus ... ok +test app::reference_authoring::tests::review_reference_facets_groups_and_attachment_hosts_browse_without_replanning_and_reject_old_facet ... ok +test app::review_navigation_tests::rejected_semantic_source_writeback_retains_review_camera_and_original_navigation ... ok +test app::review_navigation_tests::review_already_showing_the_target_keeps_its_camera_and_query ... ok +test app::review_navigation_tests::semantic_name_take_selects_the_counterpart_view_in_both_directions_with_one_exact_history_entry ... ok +test app::review_navigation_tests::semantic_review_without_a_destination_view_clears_the_old_layer_canvas_without_creating_a_view ... ok +test app::save_as_tests::save_as_late_receipt_preserves_newer_raw_source_draft_and_destination ... ok +test app::save_as_tests::save_as_preflight_rejects_historical_collision_before_starting_io ... ok +test app::save_as_tests::save_as_receipt_then_one_undo_restores_meaning_and_saving_preserves_redo ... ok +test app::source_project_tests::closed_detached_file_keeps_first_save_conflict_then_can_be_reloaded_fresh ... ok +test app::source_project_tests::inactive_invalid_draft_blocks_graphical_changes_and_diagnostic_switches_files ... ok +test app::source_project_tests::json_export_and_rejected_save_as_preserve_source_location_history_and_bytes ... ok +test app::source_project_tests::missing_include_open_keeps_old_model_and_can_be_repaired_and_saved ... ok +test app::source_project_tests::recovery_after_first_save_failure_retains_target_then_retries_without_reentering_path ... ok +test app::source_project_tests::recovery_of_previous_save_as_target_does_not_redirect_current_project ... ok +test app::source_project_tests::recovery_open_finds_first_save_journal_without_an_entry_file ... ok +test app::source_project_tests::recovery_target_survives_edit_lint_apply_and_rejected_recovery_until_success ... ok +test app::source_project_tests::source3_native_open_preserves_report_comments_nesting_and_save_reopen ... ok +test app::source_project_tests::two_file_apply_graphical_edit_exact_undo_save_as_and_reopen ... ok +test app::state_diagram_tests::all_context_dimensions_reject_stale_selection_camera_and_model_messages ... ok +test app::state_diagram_tests::create_view_places_only_its_machine_and_restores_exact_source_in_one_undo ... ok +test app::state_diagram_tests::direct_create_connect_select_and_move_keep_semantic_identity_and_atomic_history ... ok +test app::state_diagram_tests::fallback_owner_first_selection_keeps_the_captured_context_for_its_first_move ... ok +test app::state_diagram_tests::pending_drafts_block_every_direct_mutation_and_passive_camera_preserves_inputs ... ok +test app::state_diagram_tests::real_view_tool_and_history_changes_invalidate_old_release_events ... ok +test app::state_diagram_tests::rejected_creation_keeps_source_and_selection_then_valid_owner_can_retry ... ok +test app::state_diagram_tests::same_size_viewport_after_revision_or_tool_epoch_does_not_refit_the_camera ... ok +test app::state_diagram_tests::shared_semantic_states_keep_architecture_and_behavior_placements_independent ... ok +test app::state_diagram_tests::state_diagram_routes_constructor_and_source_open_to_the_existing_workspace_without_edits ... ok +test app::state_diagram_tests::state_diagram_routes_picker_and_reveal_but_explicit_architecture_does_not_bounce ... ok +test app::state_diagram_tests::state_diagram_routes_preserve_selected_raw_properties_and_pending_behavior_guards ... ok +test app::state_diagram_tests::state_navigation_catalog_selection_across_layers_resets_camera_and_tool ... ok +test app::state_diagram_tests::state_navigation_catalog_selection_of_another_machine_resets_camera_and_tool ... ok +test app::state_diagram_tests::state_navigation_layer_change_resets_the_new_machine_at_the_same_viewport_size ... ok +test app::state_diagram_tests::state_navigation_return_to_the_same_view_preserves_camera_tool_and_fit_mode ... ok +test app::state_diagram_tests::state_navigation_root_event_and_same_layer_keep_the_current_camera_and_tool ... ok +test app::transition_context_tests::explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view ... ok +test app::transition_context_tests::explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture ... ok +test app::transition_context_tests::machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera ... ok +test app::transition_context_tests::rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start ... ok +test app::transition_context_tests::scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions ... ok +test app::view_copy_tests::copying_architecture_keeps_architecture_even_with_a_retained_state_view ... ok +test app::view_copy_tests::copying_the_current_state_view_uses_its_own_identity_and_is_one_source_undo ... ok +test app::view_copy_tests::duplicate_rejection_preserves_an_incomplete_source_buffer ... ok +test app::view_copy_tests::duplicate_rejection_preserves_invalid_property_and_pending_behavior_drafts ... ok +test app::view_copy_tests::stale_copy_does_not_apply_an_unrelated_valid_property_draft ... ok +test behavior::canvas_workspace::tests::relative_camera_zoom_keeps_the_cursor_world_point_and_rejects_nonfinite_inputs ... ok +test behavior::canvas_workspace::tests::viewport_ordered_batch_uses_each_event_position_and_cancels_before_camera_motion ... ok +test behavior::contextual_source_tests::contextual_source_app_checkpoint_restores_inner_raw_draft_without_model_adoption ... ok +test behavior::contextual_source_tests::contextual_source_app_empty_actions_and_child_open_refuse_other_raw_editors ... ok +test behavior::contextual_source_tests::contextual_source_app_modal_escape_then_child_back_then_parent_cancel_preserve_accepted_source ... ok +test behavior::contextual_source_tests::contextual_source_back_remains_available_after_owner_deletion_and_with_dirty_editors ... ok +test behavior::contextual_source_tests::contextual_source_back_retains_parent_and_rejects_late_child_event ... ok +test behavior::contextual_source_tests::contextual_source_default_consumer_name_has_stable_alias_and_correlated_selection ... ok +test behavior::contextual_source_tests::contextual_source_deleted_owner_view_retains_draft_instead_of_panicking_or_using_fallback ... ok +test behavior::contextual_source_tests::contextual_source_empty_history_reports_no_change_and_success_requires_a_real_history_step ... ok +test behavior::contextual_source_tests::contextual_source_existing_choice_prunes_staged_dependency ... ok +test behavior::contextual_source_tests::contextual_source_failed_source_budget_preserves_both_drafts_and_accepted_bytes ... ok +test behavior::contextual_source_tests::contextual_source_incomplete_recovered_kind_reports_error_without_panicking ... ok +test behavior::contextual_source_tests::contextual_source_inserts_work_on_the_exact_guarded_link_and_refuses_exchange_identity_conflict ... ok +test behavior::contextual_source_tests::contextual_source_lifelines_stage_exact_separate_records_and_one_history_entry ... ok +test behavior::contextual_source_tests::contextual_source_machine_before_any_owner_creates_optional_view_atomically ... ok +test behavior::contextual_source_tests::contextual_source_mounted_small_editor_keeps_back_and_use_outside_field_scroll ... ok +test behavior::contextual_source_tests::contextual_source_nested_semantic_parent_is_explicit_and_revalidated ... ok +test behavior::contextual_source_tests::contextual_source_new_callee_lifeline_preserves_caller_bindings_as_an_explicit_warning ... ok +test behavior::contextual_source_tests::contextual_source_old_empty_action_cannot_open_a_previous_owner_or_layer ... ok +test behavior::contextual_source_tests::contextual_source_original_document_remains_bound_after_back_or_choosing_existing ... ok +test behavior::contextual_source_tests::contextual_source_owner_first_can_return_to_an_unnamed_machine_without_losing_source ... ok +test behavior::contextual_source_tests::contextual_source_recovery_keeps_inner_raw_fields_and_refuses_deleted_or_changed_owner ... ok +test behavior::contextual_source_tests::contextual_source_second_system_and_role_machine_owner_refuse_without_fallback ... ok +test behavior::contextual_source_tests::contextual_source_work_uses_reserved_contribution_and_link_ids_and_one_undo ... ok +test behavior::creation::app_tests::app_choosing_another_behavior_or_layer_cannot_commit_a_creation_draft ... ok +test behavior::creation::app_tests::app_creation_opens_the_actual_new_owner_instead_of_first_or_previously_active_behavior ... ok +test behavior::creation::app_tests::app_first_captured_escape_dismisses_file_or_pending_overlay_before_creation ... ok +test behavior::creation::app_tests::app_lesson_request_cancel_retains_creation_then_escape_discards_only_the_draft ... ok +test behavior::creation::app_tests::app_machine_behavior_submit_opens_its_opted_in_view_as_one_undoable_transaction ... ok +test behavior::creation::app_tests::app_new_never_auto_applies_valid_or_invalid_property_and_source_drafts ... ok +test behavior::creation::app_tests::app_recovered_rootless_sa_creation_retains_layer_ids_and_other_raw_drafts ... ok +test behavior::creation::app_tests::app_stale_layer_open_never_starts_or_replaces_a_draft ... ok +test behavior::creation::tests::all_four_viewpoints_are_explicit_and_survive_source_history_reopen ... ok +test behavior::creation::tests::cancel_and_illegal_family_layer_switches_never_create_model_objects ... ok +test behavior::creation::tests::corrupted_recovery_and_reserved_identity_collision_refuse_without_repairing_raw_values ... ok +test behavior::creation::tests::creation_selection_rejects_changed_scenario_body_and_post_creation_operations ... ok +test behavior::creation::tests::machine_requires_valid_owner_and_first_view_adds_no_region_or_state ... ok +test behavior::creation::tests::mounted_creation_keeps_actions_visible_and_renders_only_six_owner_candidates ... ok +test behavior::creation::tests::oa_process_and_sa_chain_keep_exact_names_root_ownership_and_one_history_entry ... ok +test behavior::creation::tests::optional_context_collapses_without_losing_authored_reference_or_raw_search ... ok +test behavior::creation::tests::owner_search_is_name_ordered_filtered_and_page_sized_not_a_catalog ... ok +test behavior::creation::tests::post_commit_selection_is_correlated_with_exact_created_behavior_and_view ... ok +test behavior::creation::tests::recovery_preserves_exact_raw_inputs_reserved_ids_and_revalidates_stale_owner ... ok +test behavior::creation::tests::rejected_source_transaction_keeps_draft_and_every_accepted_source_byte ... ok +test behavior::creation::tests::repeated_owner_names_remain_distinguishable_by_searchable_parent_context_and_alias ... ok +test behavior::direct_canvas_tests::direct_alt_execution_stops_each_operand_at_its_reply_and_marks_open_operand ... ok +test behavior::direct_canvas_tests::direct_bounds_change_during_held_drag_never_commits_old_slot ... ok +test behavior::direct_canvas_tests::direct_clicked_occurrence_capture_cancels_on_view_and_context_changes ... ok +test behavior::direct_canvas_tests::direct_context_escape_unfocus_and_pending_form_cancel_gestures ... ok +test behavior::direct_canvas_tests::direct_drag_and_click_click_use_ordered_sources_and_publish_once ... ok +test behavior::direct_canvas_tests::direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call ... ok +test behavior::direct_canvas_tests::direct_scroll_resize_and_bounds_changes_cancel_captured_slots ... ok +test behavior::direct_canvas_tests::direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot ... ok +test behavior::direct_canvas_tests::direct_selected_handle_reorders_and_hits_keep_semantic_identity ... ok +test behavior::direct_canvas_tests::reused_exchange_messages_display_their_distinct_occurrence_names ... ok +test behavior::scenario::data_contract_tests::large_payload_has_one_editor_with_reachable_field_and_authored_pages ... ok +test behavior::scenario::data_contract_tests::recursive_browsing_up_and_root_keep_values_and_report_the_depth_limit ... ok +test behavior::scenario::data_contract_tests::recursive_value_focus_exposes_authored_leaf_and_phase_changes_retain_surviving_keys ... ok +test behavior::scenario::data_contract_tests::reply_call_picker_previews_the_same_value_reset_as_selecting_another_exchange ... ok +test behavior::scenario::data_contract_tests::scenario_form_commits_two_reused_record_values_and_correlated_reply_atomically ... ok +test behavior::scenario::direct_tests::direct_busy_save_keeps_observed_scroll_current_for_the_next_pointer_gesture ... ok +test behavior::scenario::direct_tests::direct_clicked_occurrence_rejects_stale_slots_and_retains_recoverable_drafts ... ok +test behavior::scenario::direct_tests::direct_clicked_occurrences_use_root_and_operand_slots_with_exact_source_history ... ok +test behavior::scenario::direct_tests::direct_exchange_compatibility_browsing_preserves_final_slot_validation ... ok +test behavior::scenario::direct_tests::direct_exchange_picker_is_bounded_and_browsing_keeps_pending_values ... ok +test behavior::scenario::direct_tests::direct_five_hundred_exchange_choice_render_probe ... ok +test behavior::scenario::direct_tests::direct_form_keeps_the_same_canvas_in_the_widget_tree ... ok +test behavior::scenario::direct_tests::direct_middle_pair_insertion_is_one_exact_source_and_model_undo ... ok +test behavior::scenario::direct_tests::direct_operand_pair_keeps_parent_sequence_and_cannot_reorder_across_operands ... ok +test behavior::scenario::direct_tests::direct_reply_choices_cache_keeps_typing_fast_and_revalidates_changed_contexts ... ok +test behavior::scenario::direct_tests::direct_reply_choices_validate_the_actual_slot_and_reorder_rejects_reverse_order ... ok +test behavior::scenario::direct_tests::direct_scenario_reference_navigation_blocks_pending_drafts_and_stale_contexts ... ok +test behavior::scenario::direct_tests::direct_scenario_reference_navigation_retains_each_exact_caller_without_history ... ok +test behavior::scenario::direct_tests::direct_scenario_reference_return_survives_rename_and_rejects_deleted_or_changed_use ... ok +test behavior::scenario::operation_tests::operation_gui_async_picker_uses_request_root_and_nested_directions_without_erasing_rejected_values ... ok +test behavior::scenario::operation_tests::operation_gui_selects_two_parameterless_allocations_without_inventing_refinement_and_reply_derives_call ... ok +test behavior::scenario::operation_tests::operation_gui_unresolved_and_disabled_selection_recovery_preserves_rejected_raw_values ... ok +test behavior::scenario::tests::fragment_operands_accept_paired_messages_through_normal_destination_picker ... ok +test behavior::scenario::tests::graphical_call_reply_is_atomic_correlated_and_source_equivalent ... ok +test behavior::scenario::tests::invalid_lifecycle_edit_keeps_draft_and_timer_has_no_global_exchange ... ok +test behavior::scenario::tests::lifeline_picker_uses_core_viewpoint_contract ... ok +test behavior::scenario::tests::reverse_communication_canvas_choices_and_correlated_reply_share_core_direction ... ok +test behavior::scenario::tests::scenario_reuse_requires_explicit_compatible_bindings ... ok +test behavior::scenario::tests::tagged_scenario_commit_is_not_redirected_by_other_editor_events ... ok +test behavior::scenario::workspace::tests::new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session ... ok +test behavior::scenario::workspace::tests::production_empty_scenario_lifeline_tool_keeps_staged_source_inside_stable_canvas ... ok +test behavior::scenario::workspace::tests::production_scenario_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok +test behavior::scenario::workspace::tests::production_scenario_canvas_keeps_bounds_draft_identity_and_camera_through_editing ... ok +test behavior::scenario::workspace::tests::production_scenario_canvas_pan_invalidates_a_pressed_creation_and_caller_restores_once ... ok +test behavior::scenario::workspace::tests::production_scenario_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok +test behavior::scenario::workspace::tests::production_scenario_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok +test behavior::scenario::workspace::tests::production_scenario_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok +test behavior::scenario::workspace::tests::production_scenario_whole_glyph_drag_moves_one_occurrence_and_rejects_other_operand ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_chained_apply_rechecks_live_review_owner ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_missing_exact_target_leaves_other_inputs_untouched ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_returns_from_back_use_and_escape_after_initial_focus_is_consumed ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_saved_action_is_inert_on_replacement_child_review_and_changed_context ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_targets_the_name_and_rejects_stale_sessions_or_modals ... ok +test behavior::scenario::workspace::tests::scenario_workspace_choosing_tools_preserves_valid_and_invalid_pending_editors ... ok +test behavior::scenario::workspace::tests::scenario_workspace_fit_uses_measured_inner_viewport_and_rejects_nonfinite_inputs ... ok +test behavior::scenario::workspace::tests::scenario_workspace_open_and_back_restore_owner_and_mounted_camera_after_preselection ... ok +test behavior::scenario::workspace::tests::scenario_workspace_same_size_owner_and_document_changes_remeasure_fit_viewport ... ok +test behavior::scenario::workspace::tests::scenario_workspace_scaled_and_scrolled_scene_keeps_clicked_slots_and_message_endpoints ... ok +test behavior::scenario::workspace::tests::scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_identity ... ok +test behavior::scenario::workspace::tests::scenario_workspace_viewport_events_are_passive_guarded_and_do_not_apply_raw_forms ... ok +test behavior::scenario_lifecycle::tests::alternative_death_does_not_kill_its_displayed_sibling ... ok +test behavior::scenario_lifecycle::tests::conditional_birth_does_not_infer_absence_in_siblings_or_ancestors ... ok +test behavior::scenario_lifecycle::tests::explicit_receiver_is_authoritative_even_with_missing_exchange ... ok +test behavior::scenario_lifecycle::tests::lifecycle_free_fragments_do_not_poison_other_lanes ... ok +test behavior::scenario_lifecycle::tests::loop_lifetime_is_local_and_warning_bearing_loops_remain_drawable ... ok +test behavior::scenario_lifecycle::tests::missing_geometry_is_reported_without_nonfinite_or_guessed_markers ... ok +test behavior::scenario_lifecycle::tests::optional_cleanup_inside_loop_does_not_claim_balanced_or_definite_exit ... ok +test behavior::scenario_lifecycle::tests::projection_retains_exact_authored_source_and_does_not_enter_history ... ok +test behavior::scenario_lifecycle::tests::root_lifetime_has_exact_birth_death_boundaries_without_source_mutation ... ok +test behavior::scenario_lifecycle::tests::scenario_use_is_opaque_and_never_imports_callee_lifecycle_events ... ok +test behavior::scenario_scroll::tests::direct_scenario_caller_restores_mounted_scroll_once_with_clamping_and_app_receipt ... ok +test behavior::scenario_scroll::tests::direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view ... ok +test behavior::scenario_scroll::tests::direct_scroll_mounted_identity_resets_navigation_but_preserves_edits_before_first_report ... ok +test behavior::scenario_scroll::tests::direct_scroll_observer_reports_actual_initial_clamped_and_remounted_offsets_once ... ok +test behavior::scenario_scroll::tests::independent_process_owner_change_resets_actual_scroll_offsets ... ok +test behavior::state_canvas::tests::context_menu_keeps_window_anchor_and_explicit_placement_identity ... ok +test behavior::state_canvas::tests::context_tool_and_pending_form_changes_cancel_mutating_gestures ... ok +test behavior::state_canvas::tests::creation_uses_the_explicit_deepest_owner_and_never_a_fallback_ancestor ... ok +test behavior::state_canvas::tests::drag_uses_ordered_release_at_each_zoom_and_moves_descendants_in_one_undo ... ok +test behavior::state_canvas::tests::escape_and_view_switch_discard_the_armed_transition_source ... ok +test behavior::state_canvas::tests::internal_rows_and_pseudostate_hits_use_shared_persisted_geometry ... ok +test behavior::state_canvas::tests::ordered_stationary_click_does_not_move_on_later_motion_in_the_same_batch ... ok +test behavior::state_canvas::tests::pan_is_button_scoped_read_only_and_hover_does_not_reproject_the_scene ... ok +test behavior::state_canvas::tests::pseudostate_names_avoid_rightward_and_downward_routes_and_share_hit_bounds ... ok +test behavior::state_canvas::tests::relayout_does_not_turn_a_transition_click_into_a_drag_or_pan ... ok +test behavior::state_canvas::tests::selected_endpoint_reconnect_preserves_identity_and_requires_handle_motion ... ok +test behavior::state_canvas::tests::selection_relayout_does_not_move_nodes_or_change_drag_distance ... ok +test behavior::state_canvas::tests::stacked_symbol_names_avoid_each_other_and_are_independent_of_input_order ... ok +test behavior::state_canvas::tests::transition_supports_click_click_and_drag_without_coalesced_accidental_edges ... ok +test behavior::state_canvas::tests::typed_state_actions_are_named_selectable_rows_before_internal_transitions_and_regions ... ok +test behavior::state_canvas::tests::viewport_uses_full_canvas_coordinates_and_republishes_for_a_new_context ... ok +test behavior::state_edit::tests::an_empty_machine_has_an_explicit_visible_owner_for_first_region_creation ... ok +test behavior::state_edit::tests::connect_preserves_self_loops_and_rejects_hidden_or_incompatible_vertices ... ok +test behavior::state_edit::tests::creation_places_missing_ancestry_and_unfolds_existing_owners_atomically ... ok +test behavior::state_edit::tests::every_context_component_and_invalid_ownership_are_checked_before_planning ... ok +test behavior::state_edit::tests::placing_existing_content_and_moving_a_folded_group_preserve_other_views_and_semantics ... ok +test behavior::state_edit::tests::reconnect_preserves_the_complete_record_and_replaces_cardinality_in_place ... ok +test behavior::state_edit::tests::view_initializes_only_its_machine_in_one_source_and_model_history_entry ... ok +test behavior::states::tests::internal_choice_targets_self_and_edit_preserves_identity_and_description ... ok +test behavior::states::tests::invalid_pseudo_trigger_keeps_draft_and_does_not_create_orphan_event ... ok +test behavior::states::tests::selected_composite_owns_new_region_and_history_uses_selected_region ... ok +test behavior::states::tests::timeout_and_work_trigger_commit_together_roundtrip_and_undo ... ok +test behavior::tests::failed_document_commit_retains_typed_transition_and_time_event_for_repair ... ok +test behavior::tests::legacy_process_and_chain_keep_their_ordered_exchange_scene ... ok +test behavior::tests::one_behavior_form_survives_start_edit_and_unrelated_cancel_until_commit ... ok +test behavior::tests::recovery_preserves_distinct_qualified_value_paths_and_rejects_invalid_payload_atomically ... ok +test behavior::tests::recovery_retains_each_incomplete_behavior_form_without_executing_it ... ok +test behavior::tests::selecting_state_transition_keeps_its_behavior_context ... ok +test behavior::topology::contextual::tests::qualified_link_gui_deleted_transient_selection_does_not_seed_or_hide_retained_authored_endpoint ... ok +test behavior::topology::contextual::tests::qualified_link_gui_nested_preview_folds_only_the_exact_reference_instance ... ok +test behavior::topology::contextual::tests::qualified_link_gui_rejected_endpoint_keeps_exact_raw_intent_after_recovery ... ok +test behavior::topology::contextual::tests::qualified_link_gui_repeated_sources_remain_distinct_and_apply_has_exact_source_history ... ok +test behavior::topology::contextual::tests::qualified_link_gui_scope_paging_and_search_reach_tail_beyond_512_without_losing_endpoints ... ok +test behavior::topology::contextual_routing::tests::contextual_route_native_x_to_first_b_keeps_visible_arrow_above_background_and_clear_of_other_references ... ok +test behavior::topology::contextual_routing::tests::contextual_route_nested_visible_hidden_and_parallel_paths_keep_exact_addresses_and_legacy_routes ... ok +test behavior::topology::contextual_routing::tests::contextual_route_shared_anchor_preserves_selected_direct_reconnect_at_fit_and_full_scale ... ok +test behavior::topology::direct_tests::direct_process_append_preserves_existing_order_and_rejects_incompatible_exchange ... ok +test behavior::topology::direct_tests::direct_process_controls_allow_incomplete_work_and_preserve_branch_semantics ... ok +test behavior::topology::direct_tests::direct_process_empty_first_and_second_exchange_keep_the_same_interactive_renderer ... ok +test behavior::topology::direct_tests::direct_process_imported_order_is_projected_without_mutation_and_first_canvas_edit_is_atomic ... ok +test behavior::topology::direct_tests::direct_process_incompatible_imported_order_rejects_conversion_without_losing_draft ... ok +test behavior::topology::direct_tests::direct_process_nested_source_reopen_and_api_command_use_one_transaction ... ok +test behavior::topology::direct_tests::direct_process_oa_sa_repeated_occurrences_share_sources_and_split_one_guarded_link ... ok +test behavior::topology::direct_tests::direct_process_palette_preserves_fit_and_manual_camera ... ok +test behavior::topology::direct_tests::direct_process_production_click_click_drag_insert_and_scroll_coordinates ... ok +test behavior::topology::direct_tests::direct_process_production_endpoint_and_peer_reorder_handles_publish_exact_identity ... ok +test behavior::topology::direct_tests::direct_process_production_stale_focus_scroll_bounds_and_pending_cancel ... ok +test behavior::topology::direct_tests::direct_process_readable_default_fit_expansion_and_bounded_choices ... ok +test behavior::topology::direct_tests::direct_process_real_picker_stays_bounded_and_search_keeps_selection ... ok +test behavior::topology::direct_tests::direct_process_reconnect_delete_and_reorder_preserve_other_paths_and_sources ... ok +test behavior::topology::direct_tests::direct_process_reuse_preview_is_local_expandable_and_recursive_sources_are_rejected ... ok +test behavior::topology::direct_tests::direct_process_scaled_control_and_link_gestures_use_visible_positions ... ok +test behavior::topology::direct_tests::direct_process_specific_parallel_link_and_incompatible_adjacency ... ok +test behavior::topology::direct_tests::direct_process_three_ordered_exchanges_preserve_outer_paths_on_insertion ... ok +test behavior::topology::tests::cycle_projection_terminates_and_retains_actual_links ... ok +test behavior::topology::tests::empty_process_can_start_through_normal_authoring_events ... ok +test behavior::topology::tests::fitting_a_branch_keeps_all_nodes_visible_and_selection_uses_scaled_coordinates ... ok +test behavior::topology::tests::guarded_branch_is_one_transaction_with_distinct_source_identities ... ok +test behavior::topology::tests::insertion_picker_only_offers_work_matching_adjacent_exchanges ... ok +test behavior::topology::tests::legacy_branch_insertion_replaces_only_the_selected_implied_link ... ok +test behavior::topology::tests::legacy_edit_preserves_order_and_new_work_in_one_undo ... ok +test behavior::topology::tests::long_engineering_identifiers_stay_inside_label_width ... ok +test behavior::topology::tests::process_fit_canvas_selection_ignores_retained_ordered_page_scroll ... ok +test behavior::topology::tests::process_fit_disclosures_and_focus_preserve_rejected_raw_draft_and_accepted_source ... ok +test behavior::topology::tests::process_fit_uses_measured_height_and_reports_minimum_without_changing_manual_zoom ... ok +test behavior::topology::tests::reusable_preview_preserves_identity_and_filters_recursive_sources ... ok +test behavior::topology::tests::stale_or_invalid_submit_preserves_inputs_and_creates_nothing ... ok +test behavior::topology::tests::unlinked_contributions_do_not_gain_fictional_edges ... ok +test behavior::topology::view::layout_tests::production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_review_owners ... ok +test behavior::topology::view::layout_tests::production_process_caller_return_rejects_its_previous_camera_lifetime ... ok +test behavior::topology::view::layout_tests::production_process_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok +test behavior::topology::view::layout_tests::production_process_canvas_focus_wheel_fit_and_stale_camera_are_presentation_only ... ok +test behavior::topology::view::layout_tests::production_process_canvas_scaled_path_creation_preserves_source_and_editor_bounds ... ok +test behavior::topology::view::layout_tests::production_process_form_replacement_resets_scroll_without_a_closed_render ... ok +test behavior::topology::view::layout_tests::production_process_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok +test behavior::topology::view::layout_tests::production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok +test behavior::topology::view::layout_tests::production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok +test connection_edit::tests::cancel_navigation_revision_and_dirty_drafts_cannot_commit_a_stale_connection ... ok +test connection_edit::tests::visible_endpoint_drawing_commits_ports_and_exchange_once_with_exact_history ... ok +test connection_tool::tests::boundary_candidates_match_core_orientation_and_reject_same_owner_or_mixed_kind ... ok +test connection_tool::tests::boundary_feasibility_keeps_interface_warnings_and_final_carried_item_checks ... ok +test connection_tool::tests::boundary_feasibility_rejects_dangling_identity_in_a_malformed_element_map ... ok +test connection_tool::tests::direct_connections_share_exact_source_history_and_model_parity ... ok +test connection_tool::tests::explicit_function_ports_keep_owners_contracts_and_do_not_invent_carried_items ... ok +test connection_tool::tests::final_command_validation_rejects_invalid_models_without_mutation ... ok +test connection_tool::tests::function_cards_use_unique_reuse_and_atomic_generation_when_ambiguous ... ok +test connection_tool::tests::mismatched_port_owners_layers_and_contract_kinds_are_not_candidates ... ok +test connection_tool::tests::operational_communication_uses_participants_without_creating_ports ... ok +test connection_tool::tests::operational_interaction_targets_only_distinct_oa_activities ... ok +test context_menu::tests::a_new_context_token_cannot_reuse_the_previous_mouse_press_or_scroll ... ok +test context_menu::tests::actions_area_does_not_swallow_first_independent_click_after_overlay_consumed_its_release ... ok +test context_menu::tests::anchors_outside_the_window_and_nonfinite_input_stay_bounded ... ok +test context_menu::tests::clicking_disabled_or_dragging_off_an_action_never_executes_it ... ok +test context_menu::tests::coalesced_dismissal_release_does_not_swallow_the_next_independent_click ... ok +test context_menu::tests::empty_and_disabled_only_menus_still_capture_keys_and_escape_closes ... ok +test context_menu::tests::keyboard_skips_headers_separators_and_disabled_actions_and_invokes_once ... ok +test context_menu::tests::outside_dismissal_consumes_press_and_release_across_controlled_close ... ok +test context_menu::tests::reconstructed_overlay_uses_retained_layout_for_rows_and_hit_testing ... ok +test context_menu::tests::retained_overlay_layout_keeps_measured_width_and_scrollbar_after_reconstruction ... ok +test context_menu::tests::tall_menu_clamps_to_viewport_and_keyboard_wheel_and_scrollbar_reach_the_end ... ok +test data_values::tests::phase_choices_keep_reused_record_addresses_distinct ... ok +test data_values::tests::recursive_picker_starts_with_distinct_leaf_names_and_sorts_them ... ok +test data_values::tests::recursive_records_can_be_drilled_into_and_valid_imported_paths_resolve ... ok +test data_values::tests::reference_parameter_stops_descent_and_unspecified_direction_remains_visible ... ok +test data_values::tests::roots_and_one_hop_children_do_not_expand_unrelated_payload_branches ... ok +test data_view::tests::allocation_rows_edit_relationship_identity_and_remove_only_that_allocation ... ok +test data_view::tests::association_creation_is_a_separate_relation_with_editable_end_contracts ... ok +test data_view::tests::browsing_pages_preserves_invalid_numeric_draft_and_inherited_allocation_identity ... ok +test data_view::tests::contextual_data_actions_use_typed_defaults_and_one_undo ... ok +test data_view::tests::large_inherited_dictionary_has_bounded_widgets_and_every_original_field_is_reachable ... ok +test diagram::lifecycle_tests::menu_reset_and_draw_use_fresh_open_context ... ok +test diagram::lifecycle_tests::middle_and_arrow_panning_preserve_source_and_history ... ok +test diagram::lifecycle_tests::motion_after_release_must_not_move_clicked_port ... ok +test diagram::lifecycle_tests::motion_after_source_click_release_must_not_create_connection ... ok +test diagram::pointer_tests::coalesced_fe_ports_commit_once_through_app_and_source_history ... ok +test diagram::pointer_tests::coalesced_port_drag_commits_once_through_app_and_source_history ... ok +test diagram::pointer_tests::drag_from_already_selected_stationary_port_keeps_source_between_batches ... ok +test diagram::pointer_tests::menu_captured_motion_must_not_turn_following_click_into_old_port_drag ... ok +test diagram::pointer_tests::modal_and_stale_epoch_block_queued_connection_and_port_commits ... ok +test diagram::port_hit_tests::coincident_ports_keep_selection_then_use_stable_identity ... ok +test diagram::port_hit_tests::nearest_port_center_wins_on_all_borders_and_zoom_levels ... ok +test diagram::port_hit_tests::nested_owner_palette_selection_drag_and_context_menu_pick_the_same_port ... ok +test diagram::port_labels::tests::leaving_and_reentering_keeps_suppression_until_each_held_button_releases ... ok +test diagram::port_labels::tests::menu_epoch_clears_suppression_when_the_overlay_consumes_release ... ok +test diagram::port_labels::tests::outside_presses_do_not_suppress_and_cancel_or_focus_loss_clear_held_buttons ... ok +test diagram::port_labels::tests::passive_layer_leaves_port_drag_selection_and_commit_with_the_interactive_canvas ... ok +test diagram::tests::association_end_contracts_share_svg_geometry_and_select_without_moving_name_label ... ok +test diagram::tests::bend_gestures_use_screen_hit_radius_and_commit_one_undo_at_three_zooms ... ok +test diagram::tests::coincident_line_hit_retains_the_selected_connection ... ok +test diagram::tests::connection_gestures_pick_real_ports_and_commit_coalesced_drag_at_every_zoom ... ok +test diagram::tests::detached_label_selects_connection_and_occluding_card_owns_its_hit ... ok +test diagram::tests::dragging_a_child_previews_the_same_enclosure_and_ports_as_commit ... ok +test diagram::tests::fast_drags_commit_the_release_cursor_without_a_final_motion_event ... ok +test diagram::tests::implicit_port_click_selects_semantics_and_drag_preview_svg_share_anchors ... ok +test diagram::tests::route_cache_ignores_pan_zoom_and_cancelled_drag_cannot_commit ... ok +test diagram::tests::routed_svg_has_same_polyline_label_and_bounds_as_canvas_scene ... ok +test diagram::tests::svg_distinguishes_inferred_work_allocation_from_direct_allocation ... ok +test diagram::tests::svg_preserves_visible_exchange_labels_and_capability_stereotypes ... ok +test diagram::tests::typed_function_glyphs_and_unoriented_standard_connections_have_distinct_svg ... ok +test diagram::view_options_tests::communication_svg_uses_saved_visibility_labels_and_direction ... ok +test document::save_as_tests::save_as_preserves_explicit_source_entry_changes_in_history ... ok +test document::save_as_tests::save_as_rebases_uniform_history_without_an_undo_step_or_lost_redo ... ok +test document::save_as_tests::save_as_rejects_included_filename_collisions_in_past_and_future_without_mutation ... ok +test document::save_as_tests::save_as_rejects_stale_or_content_changing_receipt_atomically ... ok +test document::tests::accepted_history_keeps_the_latest_hundred_complete_snapshots ... ok +test document::tests::altered_compiled_model_or_index_is_rejected_without_mutation ... ok +test document::tests::asynchronous_save_marks_its_snapshot_without_cleaning_later_edits ... ok +test document::tests::byte_budget_trims_oldest_history_without_discarding_a_large_current ... ok +test document::tests::cached_view_index_tracks_accepted_creation_undo_and_redo ... ok +test document::tests::comment_and_semantic_history_restore_exact_bytes_and_branch_together ... ok +test document::tests::creation_selects_the_single_uuid_persisted_in_source ... ok +test document::tests::invalid_project_is_rejected_before_format_can_normalize_it ... ok +test document::tests::nested_cross_file_reparent_and_source_edit_share_exact_history ... ok +test document::tests::no_op_command_or_source_apply_preserves_history_revision_and_redo ... ok +test document::tests::omitted_id_stabilization_remains_dirty_until_exact_source_is_saved ... ok +test document::tests::recreated_equal_record_does_not_inherit_deleted_source_ownership ... ok +test document::tests::rejected_compile_command_and_writeback_preserve_state_and_redo ... ok +test explorer::tests::collapse_and_search_retain_context_without_changing_collapse_state ... ok +test explorer::tests::deep_containment_and_search_keep_the_full_path_without_recursion ... ok +test explorer::tests::equal_names_have_stable_alias_and_identity_tie_breakers ... ok +test explorer::tests::perspective_isolation_and_allocations_do_not_change_containment ... ok +test explorer::tests::prefix_names_cannot_separate_a_parent_from_its_children ... ok +test forms::data_fields::tests::defaults_distinguish_absent_empty_and_typed_literals_with_escaped_unicode ... ok +test forms::data_fields::tests::field_browser_pages_every_direct_field_without_flattening_other_records ... ok +test forms::data_fields::tests::independent_review_combined_take_type_informs_actual_default_form_in_both_directions ... ok +test forms::data_fields::tests::numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state ... ok +test forms::data_fields::tests::selecting_qualified_path_preserves_expression_typed_before_the_next_render ... ok +test forms::data_fields::tests::semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep ... ok +test forms::data_fields::tests::supertype_picker_excludes_descendants_and_keeps_original_inherited_field_identity ... ok +test forms::history_fields::tests::history_absent_baseline_and_empty_history_use_explicit_facts_and_exact_document_history ... ok +test forms::history_fields::tests::history_clear_discards_inactive_invalid_constructors_before_reusing_the_same_row ... ok +test forms::history_fields::tests::history_compound_value_and_constructor_variants_are_editable_without_live_context ... ok +test forms::history_fields::tests::history_deleted_keys_and_observation_uuids_remain_exact_and_duplicate_history_rejects ... ok +test forms::history_fields::tests::history_every_descriptor_fact_choice_matches_actual_closed_types_and_source_contract ... ok +test forms::history_fields::tests::history_exact_strings_do_not_confuse_uuid_shaped_prose_or_literal_keys_with_references ... ok +test forms::history_fields::tests::history_invalid_numbers_survive_constructor_switch_reorder_removal_and_recovery ... ok +test forms::history_fields::tests::history_missing_semantic_field_can_be_added_and_removed_without_touching_prose_or_live_objects ... ok +test forms::history_fields::tests::history_nested_optional_number_events_switch_absent_present_and_recover_invalid_raw ... ok +test forms::history_fields::tests::history_nested_uuid_and_list_events_preserve_present_values_and_tagged_item_branches ... ok +test forms::history_fields::tests::history_optional_uuid_and_text_seed_absence_without_inventing_observations ... ok +test forms::history_fields::tests::history_primitive_type_choices_remain_visible_inside_the_tagged_union ... ok +test forms::history_fields::tests::history_retained_text_constructors_follow_exact_mode_without_changing_the_observation ... ok +test forms::port_fields::tests::all_demo_records_roundtrip_through_graphical_and_escaped_string_drafts ... ok +test forms::port_fields::tests::boundary_choices_honor_draft_direction_same_kind_and_direct_interface_items ... ok +test forms::port_fields::tests::contract_choices_follow_owner_and_never_offer_legacy_or_oa_typed_ports ... ok +test forms::port_fields::tests::failed_shape_keeps_incomplete_text_and_saved_contract_payloads ... ok +test forms::port_fields::tests::functional_endpoints_require_owner_direction_and_items_but_retain_bad_current_choice ... ok +test forms::port_fields::tests::interface_roles_offer_participants_and_component_ports_independent_of_orientation ... ok +test forms::port_fields::tests::port_allocation_choices_use_actual_owner_direction_and_ignore_edited_relation ... ok +test forms::port_fields::tests::standard_switch_retains_incompatible_orientation_until_explicit_repair ... ok +test forms::port_fields::tests::switching_function_direction_preserves_invalid_items_and_each_authored_payload ... ok +test forms::reference_chooser::tests::captured_choice_cannot_cross_document_revision_draft_record_or_array_path ... ok +test forms::reference_chooser::tests::catalog_search_disambiguates_repeated_names_containment_alias_and_exact_placement_identity ... ok +test forms::reference_chooser::tests::dismissal_swallows_matching_release_across_closed_rebuild_but_next_click_reaches_parent ... ok +test forms::reference_chooser::tests::empty_search_escape_tab_and_optional_clear_do_not_silently_choose_nil ... ok +test forms::reference_chooser::tests::historical_paths_never_open_and_only_explicit_choice_or_clear_is_a_write ... ok +test forms::reference_chooser::tests::mounted_clipboard_paste_and_ime_commit_preserve_whole_strings_without_selecting ... ok +test forms::reference_chooser::tests::mounted_filtered_row_click_uses_the_displayed_identity_and_scoped_write_intent ... ok +test forms::reference_chooser::tests::mounted_input_preserves_rapid_same_batch_typing_and_cursor_across_controlled_rebuilds ... ok +test forms::reference_chooser::tests::mounted_keyboard_pages_and_selects_only_the_highlighted_visible_candidate_once ... ok +test forms::reference_chooser::tests::mounted_overlay_clamps_at_three_field_positions_and_keeps_results_and_query_bounded ... ok +test forms::reference_chooser::tests::mounted_query_change_rejects_old_release_and_old_result_events_without_locking_popup ... ok +test forms::reference_chooser::tests::mounted_query_then_escape_in_one_batch_closes_and_relative_keys_keep_input_order ... ok +test forms::reference_chooser::tests::out_of_order_query_serials_cannot_restore_obsolete_results ... ok +test forms::tests::data_and_port_choices_are_typed_but_retain_invalid_current_values ... ok +test forms::tests::editing_allocation_offers_other_performers_without_counting_itself_twice ... ok +test forms::tests::moving_placement_and_changing_identity_is_one_lossless_undoable_edit ... ok +test forms::tests::parent_choices_reject_cycles_without_removing_clear_or_current_choice ... ok +test forms::tests::reply_and_reuse_forms_offer_semantically_compatible_references ... ok +test forms::tests::scenario_choices_respect_owner_and_exchange_layer ... ok +test forms::tests::semantic_port_group_choices_use_combined_owner_and_retain_invalid_transferred_items ... ok +test forms::tests::shared_perspective_change_keeps_payload_and_rejects_incompatible_dependencies ... ok +test forms::tests::state_observation_picker_keeps_work_lifelines_allocated_to_machine_owner ... ok +test forms::tests::state_placement_form_uses_draft_destination_membership_without_changing_other_views ... ok +test forms::tests::state_projection_form_keeps_string_variants_and_requires_a_typed_context ... ok +test forms::view_fields::tests::category_family_change_retains_members_until_explicit_correction ... ok +test forms::view_fields::tests::selective_view_property_apply_recovery_and_undo_preserve_semantics_and_empty_choices ... ok +test guide::tests::filtering_and_related_navigation_keep_selection_visible ... ok +test guide::tests::guide_topic_navigation_resets_only_article_and_same_topic_search_keeps_scroll ... ok +test guide::tests::search_finds_concepts_in_walkthroughs_and_api_reference ... ok +test home::tests::corrupt_future_and_oversized_history_are_preserved_with_usable_session_history ... ok +test home::tests::failed_metadata_replace_keeps_previous_history_and_reports_memory_only_entry ... ok +test home::tests::first_and_replaced_metadata_are_parseable_and_contain_no_model_bytes ... ok +test home::tests::forgotten_and_missing_files_never_change_the_model_or_other_entries ... ok +test home::tests::loading_missing_history_does_not_create_files_or_directories ... ok +test home::tests::malformed_entries_and_unknown_fields_never_become_open_requests ... ok +test home::tests::successful_local_history_is_bounded_ordered_deduplicated_and_durable ... ok +test home::tests::unavailable_location_can_reappear_on_refresh_without_losing_identity ... ok +test home::tests::walkthrough_uses_packaged_bundle_before_development_and_reports_missing_assets ... ok +test hosted::client::tests::hosted_endpoint_validation_redacts_secrets_and_preserves_proxy_paths ... ok +test hosted::client::tests::hosted_lost_publication_response_is_uncertain_without_retry ... ok +test hosted::client::tests::hosted_open_rejects_incoherent_envelopes_missing_etags_and_unstable_ids ... ok +test hosted::client::tests::hosted_oversized_local_bundle_is_rejected_before_network_admission ... ok +test hosted::client::tests::hosted_production_auth_proxy_prefix_and_located_rejection ... ok +test hosted::client::tests::hosted_production_roundtrip_preserves_comments_noops_and_conflicts ... ok +test hosted::client::tests::hosted_publication_race_keeps_receipt_identity_for_review ... ok +test hosted::client::tests::hosted_publication_retains_known_resource_after_failed_followup ... ok +test hosted::client::tests::hosted_put_preserves_an_opaque_etag_and_compiles_the_accepted_bundle ... ok +test hosted::client::tests::hosted_redirect_is_not_followed_or_retried_and_problem_tokens_are_redacted ... ok +test hosted::client::tests::hosted_response_limits_cover_declared_and_chunked_bodies ... ok +test hosted::client::tests::hosted_structured_mutation_failures_distinguish_precommit_rejections ... ok +test hosted::client::tests::hosted_timeout_and_disconnect_leave_mutation_outcomes_explicit ... ok +test model_menu::tests::add_bend_here_uses_context_point_and_one_source_aware_history_change ... ok +test model_menu::tests::late_menu_actions_do_not_dismiss_new_menu_or_cross_view_context ... ok +test model_menu::tests::menu_preserves_pending_properties_and_rejects_delayed_mutations ... ok +test model_menu::tests::pending_source_allows_file_navigation_but_disables_model_actions ... ok +test model_menu::tests::right_click_targets_actor_without_changing_selection_until_action ... ok +test model_menu::tests::show_from_explorer_places_unfolds_and_keeps_semantic_selection ... ok +test model_menu::tests::state_menu_folds_and_hides_only_its_captured_placement_and_rejects_stale_views ... ok +test model_menu::tests::state_menu_keeps_typed_creation_reachable_and_preserves_pending_properties ... ok +test navigation::tests::semantic_browser_finds_behavior_users_without_confusing_containment ... ok +test port_edit::tests::all_border_drags_commit_exact_preview_and_restore_automatic_anchors_in_history ... ok +test port_edit::tests::graphical_port_reparent_clears_old_anchors_in_the_same_history_operation ... ok +test port_preview::tests::functional_creation_identifies_new_and_reused_endpoints_and_owners ... ok +test port_preview::tests::legacy_split_discloses_endpoint_routing_item_origins_and_retained_evidence ... ok +test port_preview::tests::reused_boundary_shows_item_union_and_existing_allocating_facts ... ok +test port_preview::tests::synchronization_preview_exposes_contract_shrinkage_and_current_declaration_origin ... ok +test port_workflow::recovery_tests::port_recovery_retains_authored_contract_and_identity_but_rebuilds_preview ... ok +test port_workflow::tests::creating_typed_port_and_rejecting_stale_preview_preserve_drafts_and_identity ... ok +test port_workflow::tests::graphical_function_exchange_and_component_generation_commit_and_undo_whole_contracts ... ok +test port_workflow::tests::legacy_resolution_preserves_original_record_and_applies_as_one_undo ... ok +test port_workflow::tests::port_creation_keeps_owner_diagram_and_unfolds_in_the_same_undo ... ok +test port_workflow::tests::synchronization_uses_active_contract_instead_of_preserved_direction ... ok +test recovery::source5::tests::source5_fingerprint_preserves_historical_field_order_and_diagram_shape ... ok +test recovery::source5_tests::source4_checkpoint_remains_explicitly_unsupported_without_touching_disk ... ok +test recovery::source5_tests::source5_checkpoint_does_not_refresh_unrelated_clean_buffer_baseline ... ok +test recovery::source5_tests::source5_checkpoint_missing_identity_is_not_a_lossless_upgrade ... ok +test recovery::source5_tests::source5_checkpoint_old_diagram_property_shape_remains_editable ... ok +test recovery::source5_tests::source5_checkpoint_port_draft_replans_and_rejects_stale_base ... ok +test recovery::source5_tests::source5_checkpoint_preserves_dirty_invalid_text_and_baseline_exactly ... ok +test recovery::source5_tests::source5_checkpoint_preserves_distinct_unapplied_open_workspace ... ok +test recovery::source5_tests::source5_checkpoint_property_preserves_invalid_raw_input ... ok +test recovery::source5_tests::source5_checkpoint_upgrade_never_rewrites_checkpoint_or_origin ... ok +test recovery::source5_tests::source5_checkpoint_upgrades_only_header_and_clean_editor_baseline ... ok +test recovery::source5_tests::source5_checkpoint_workflow_rebinds_only_the_proven_old_revision ... ok +test recovery::source7_tests::source7_current_combined_contextual_and_operation_drafts_survive_without_execution ... ok +test recovery::source7_tests::source7_explicit_new_fields_cannot_enter_frozen_old_model_profiles ... ok +test recovery::source7_tests::source7_legacy_inactive_constructor_path_is_structural_but_raw_prose_is_not ... ok +test recovery::source7_tests::source7_legacy_profile_workflow_and_semantic_drafts_replan_with_exact_base ... ok +test recovery::source7_tests::source7_legacy_profiles_preserve_dirty_source_property_and_behavior_raw_fields ... ok +test recovery::source7_tests::source7_legacy_structural_intent_rejects_even_null_but_never_scans_raw_strings ... ok +test recovery::source7_tests::source7_legacy_view_selector_refuses_new_relation_kind_without_rewriting_checkpoint ... ok +test recovery::source7_tests::source7_recovers_nonempty_frozen_5_and_6_profiles_with_exact_historical_binding ... ok +test recovery::tests::altered_accepted_identity_and_unrecognized_raw_property_fields_reject ... ok +test recovery::tests::exact_source_and_unfinished_invalid_buffers_restore_without_model_overwrite ... ok +test recovery::tests::excess_retained_buffers_cannot_replace_the_last_recoverable_checkpoint ... ok +test recovery::tests::invalid_open_retains_distinct_accepted_model_and_repair_workspace ... ok +test recovery::tests::missing_includes_remain_reachable_repair_targets_without_fabricated_buffers ... ok +test recovery::tests::multiple_sessions_replace_only_their_own_checkpoint_and_corruption_is_retained ... ok +test recovery::tests::raw_invalid_property_number_survives_and_stays_rejected_until_repaired ... ok +test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_draft_cannot_target_an_unreserved_or_non_artifact_identity ... ok +test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_raw_variants_duplicate_addresses_and_unknown_fields_are_refused ... ok +test semantic_review::artifacts_recovery_falsifier_tests::forged_artifact_reservations_cannot_duplicate_or_claim_an_existing_identity_or_another_owner ... ok +test semantic_review::artifacts_recovery_falsifier_tests::legitimate_invalid_artifact_checkpoint_round_trips_and_prepares_only_after_explicit_repair ... ok +test semantic_review::artifacts_tests::artifact_candidate_is_a_complete_record_not_a_tombstone_or_unattached_creation_shortcut ... ok +test semantic_review::artifacts_tests::artifact_creation_and_identified_source_deletion_follow_core_metadata_and_history_boundary_both_directions ... ok +test semantic_review::artifacts_tests::artifact_gui_reserves_recovers_and_creates_only_after_all_seven_metadata_reviews_both_directions ... ok +test semantic_review::artifacts_tests::artifact_metadata_apply_and_exact_history_reset_are_one_source_preserving_undo_redo ... ok +test semantic_review::artifacts_tests::artifact_move_metadata_none_empty_and_invalid_raw_recovery_are_independent_both_directions ... ok +test semantic_review::artifacts_tests::artifact_moved_source_absence_deletes_but_destination_only_take_retains_both_directions ... ok +test semantic_review::attachments::tests::absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exact_legacy_base ... ok +test semantic_review::attachments::tests::addressed_attachment_candidates_equal_direct_core_oracle_both_directions ... ok +test semantic_review::attachments::tests::attachment_apply_is_one_exact_source_and_history_undo_redo ... ok +test semantic_review::attachments::tests::attachment_candidate_and_direct_record_keep_conflict_on_actual_slot_without_losing_drafts ... ok +test semantic_review::attachments::tests::attachment_pair_drafts_survive_family_switch_and_restore_without_mutating_model ... ok +test semantic_review::attachments::tests::functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate ... ok +test semantic_review::attachments::tests::parent_and_each_endpoint_candidate_preserve_other_fields_and_match_core_both_directions ... ok +test semantic_review::attachments::tests::raw_rejected_attachment_reference_survives_recovery_and_wrong_scope_is_rejected ... ok +test semantic_review::attachments::tests::source_only_absence_and_explicit_absent_endpoint_are_different ... ok +test semantic_review::attachments::tests::two_incident_endpoint_candidates_form_one_complete_record_and_recover_invalid_excluded_draft ... ok +test semantic_review::batch_tests::batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip ... ok +test semantic_review::batch_tests::batch_candidate_conflict_keeps_both_raw_values_and_keep_is_a_real_constraint ... ok +test semantic_review::batch_tests::batch_complete_constructor_cannot_erase_another_rows_new_literal ... ok +test semantic_review::batch_tests::batch_constructor_retirement_conflicts_with_another_rows_kept_bound_in_both_orders ... ok +test semantic_review::batch_tests::batch_delayed_text_targets_its_original_row_and_facet_without_switching_focus ... ok +test semantic_review::batch_tests::batch_excluded_invalid_row_survives_focus_reopen_and_v2_recovery ... ok +test semantic_review::batch_tests::batch_first_equal_constructor_acknowledgement_allows_an_independent_bound ... ok +test semantic_review::batch_tests::batch_joint_bounds_use_one_candidate_and_match_an_independent_atomic_core_oracle ... ok +test semantic_review::batch_tests::batch_reverse_distinct_targets_work_and_reverse_convergence_is_explicit ... ok +test semantic_review::batch_tests::batch_v2_rejects_duplicate_included_sources_bad_addresses_and_stale_model ... ok +test semantic_review::batch_tests::category_family_and_members_are_independent_core_checked_facets_both_directions ... ok +test semantic_review::batch_tests::recovered_trace_picker_names_existing_trace_instead_of_deleted_reference ... ok +test semantic_review::batch_tests::rejected_direction_switch_at_row_limit_preserves_every_recoverable_draft ... ok +test semantic_review::compose::diagnostic_tests::distinct_review_conflict_keeps_trace_aliases_even_when_names_are_equal ... ok +test semantic_review::compose::diagnostic_tests::reserved_creation_conflict_uses_its_rows_seed_instead_of_a_deleted_reference ... ok +test semantic_review::compose::diagnostic_tests::same_review_presence_and_owner_conflict_names_both_facets_and_owners_and_retains_drafts ... ok +test semantic_review::compose::diagnostic_tests::typed_owner_display_does_not_reinterpret_uuid_shaped_artifact_text ... ok +test semantic_review::constructor_tests::closed_data_and_event_take_composition_matches_core_across_every_kind_pair_and_direction ... ok +test semantic_review::constructor_tests::data_group_candidates_cover_every_definition_and_companion_default_both_directions ... ok +test semantic_review::constructor_tests::data_group_retains_invalid_sibling_drafts_companion_default_and_variant_editor_buffers ... ok +test semantic_review::constructor_tests::exchange_constructor_takes_qualified_annotation_addresses_without_remapping_expression_text ... ok +test semantic_review::constructor_tests::existing_fragment_and_branch_group_candidates_edit_complete_owned_order_without_deletion ... ok +test semantic_review::constructor_tests::group_take_reference_payload_and_scalar_candidate_match_core_and_one_document_batch ... ok +test semantic_review::constructor_tests::historical_event_keep_retires_only_obsolete_branch_and_unchanged_kind_acknowledges_no_payload ... ok +test semantic_review::constructor_tests::historical_same_kind_take_preserves_target_only_literals_when_composed_with_candidate ... ok +test semantic_review::constructor_tests::occurrence_and_event_candidate_variants_cover_closed_families_without_touching_headers ... ok +test semantic_review::constructor_tests::outer_and_nested_constructor_candidates_require_explicit_resolution_and_retain_both_drafts ... ok +test semantic_review::constructor_tests::port_group_companions_typed_owner_context_and_direction_restrictions_are_explicit ... ok +test semantic_review::constructor_tests::reverse_existing_typed_port_reviews_keep_or_candidate_legacy_without_permitting_typed_oa_write ... ok +test semantic_review::constructor_tests::scenario_use_group_takes_typed_binding_references_and_preserves_source_text_both_directions ... ok +test semantic_review::keyed_rows_tests::annotation_address_browser_descends_qualified_fields_without_changing_rows ... ok +test semantic_review::keyed_rows_tests::annotation_observations_keep_uuid_shaped_expression_text_in_both_sides_and_history ... ok +test semantic_review::keyed_rows_tests::batch_keyed_rows_compare_exact_target_slots_and_retain_independent_values ... ok +test semantic_review::keyed_rows_tests::binding_choices_separate_reused_keys_from_local_lifelines ... ok +test semantic_review::keyed_rows_tests::binding_local_picker_follows_a_candidate_parent_before_the_review_is_valid ... ok +test semantic_review::keyed_rows_tests::binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenario ... ok +test semantic_review::keyed_rows_tests::independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_directions ... ok +test semantic_review::keyed_rows_tests::keyed_recovery_retains_raw_rows_and_staged_pairs_both_directions_without_applying ... ok +test semantic_review::keyed_rows_tests::keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_staging ... ok +test semantic_review::keyed_rows_tests::keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exact_reset ... ok +test semantic_review::keyed_rows_tests::multiple_new_rows_compose_in_empty_collections_using_destination_key_order ... ok +test semantic_review::keyed_rows_tests::row_review_apply_preserves_exact_source_and_semantic_history_through_undo_redo ... ok +test semantic_review::keyed_tests::deleted_member_source_observation_removes_only_mapped_membership_both_directions ... ok +test semantic_review::keyed_tests::exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss ... ok +test semantic_review::keyed_tests::exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions ... ok +test semantic_review::keyed_tests::function_port_members_use_existing_typed_choices_and_preserve_oa_legacy ... ok +test semantic_review::keyed_tests::historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution ... ok +test semantic_review::keyed_tests::invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable ... ok +test semantic_review::keyed_tests::member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys ... ok +test semantic_review::keyed_tests::reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft ... ok +test semantic_review::keyed_tests::staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence ... ok +test semantic_review::keyed_tests::stale_or_source_failed_reset_and_membership_apply_retain_exact_choices ... ok +test semantic_review::keyed_tests::unordered_members_keep_take_candidate_preserve_other_rows_both_directions ... ok +test semantic_review::keyed_tests::unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority ... ok +test semantic_review::message_kind_tests::message_group_candidate_closed_kinds_are_core_validated_in_both_directions ... ok +test semantic_review::message_kind_tests::message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit ... ok +test semantic_review::message_kind_tests::message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement ... ok +test semantic_review::message_kind_tests::message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects ... ok +test semantic_review::message_kind_tests::message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer ... ok +test semantic_review::message_kind_tests::message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions ... ok +test semantic_review::message_kind_tests::message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions ... ok +test semantic_review::message_kind_tests::message_group_timer_does_not_erase_exchange_without_its_own_selected_decision ... ok +test semantic_review::ordered_tests::ambiguous_order_preserves_local_member_and_requires_explicit_candidate_positions ... ok +test semantic_review::ordered_tests::historical_pair_restore_uses_exact_member_keys_and_never_zips_order_observations ... ok +test semantic_review::ordered_tests::order_take_preserves_destination_only_positions_and_order_alone_cannot_add_members ... ok +test semantic_review::ordered_tests::ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes ... ok +test semantic_review::ordered_tests::ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_take_reorders_only ... ok +test semantic_review::ordered_tests::ordered_review_apply_is_exact_source_history_transaction_with_reopen_and_undo_redo ... ok +test semantic_review::ordered_tests::ordered_take_uses_unique_anchors_and_matches_core_with_unrelated_candidate_both_directions ... ok +test semantic_review::ordered_tests::raw_order_survives_membership_changes_decisions_and_recovery_until_explicit_reconcile ... ok +test semantic_review::ordered_tests::several_incoming_members_follow_source_order_despite_independent_identity_sorting ... ok +test semantic_review::recovery::tests::raw_scalar_and_closed_constructor_drafts_recover_against_exact_model_only ... ok +test semantic_review::recovery::tests::version_one_without_orders_restores_raw_intent_and_replans_without_execution ... ok +test semantic_review::source7_tests::contextual_incident_leaf_keep_allows_path_change_but_conflicts_with_changed_leaf_both_directions ... ok +test semantic_review::source7_tests::reverse_operation_omission_gui_requires_raw_rationale_and_matches_one_core_batch_after_recovery ... ok +test semantic_review::source7_tests::semantic_operation_virtual_none_candidate_uses_exact_allocation_and_preserves_accepted_bytes ... ok +test semantic_review::tests::candidate_mixed_with_reference_take_uses_typed_correspondence_not_text_guessing ... ok +test semantic_review::tests::deselected_invalid_candidate_is_retained_but_does_not_poison_another_selected_scalar ... ok +test semantic_review::tests::explicit_trace_selection_updates_only_that_history ... ok +test semantic_review::tests::first_equal_constructor_acknowledges_only_kind_and_allows_independent_leaves ... ok +test semantic_review::tests::independent_review_clear_bound_ignores_retained_invalid_other_facet ... ok +test semantic_review::tests::independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity ... ok +test semantic_review::tests::inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approval ... ok +test semantic_review::tests::invalid_numeric_draft_and_protected_edits_retain_workflow_and_source ... ok +test semantic_review::tests::read_only_structured_facts_and_partial_rollup_are_honest ... ok +test semantic_review::tests::relation_candidates_preserve_endpoints_and_combine_selected_roles_and_bounds ... ok +test semantic_review::tests::scalar_keep_take_candidate_commit_exact_source_and_history_in_both_directions ... ok +test semantic_review::tests::scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers ... ok +test semantic_review::tests::stale_plan_and_source_writeback_failure_do_not_lose_decisions_or_history ... ok +test semantic_review::tests::state_kind_scalar_uses_typed_candidate_and_core_validation ... ok +test semantic_review::view::reserved_artifact_tests::reserved_artifact_facet_and_reference_labels_survive_recovery_without_calling_it_deleted ... ok +test source_navigation::tests::search_uses_utf8_columns_wraps_and_refreshes_after_edits ... ok +test source_tree::tests::active_scroll_offset_uses_the_visible_order_and_retains_ancestor_context ... ok +test source_tree::tests::collapsed_folders_retain_active_dirty_and_diagnostic_markers ... ok +test source_tree::tests::deep_paths_build_expand_and_drop_on_a_small_stack ... ok +test source_tree::tests::detached_buffers_keep_paths_and_only_clean_buffers_are_closable ... ok +test source_tree::tests::exact_unicode_spaces_and_file_folder_name_collisions_are_not_normalized ... ok +test source_tree::tests::folders_are_compact_and_sorted_before_files_without_losing_duplicate_basenames ... ok +test source_workspace::tests::draft_history_budget_covers_inactive_files_without_discarding_drafts ... ok +test source_workspace::tests::switching_retains_invalid_drafts_and_independent_history ... ok +test tests::actor_port_creation_updates_containment_and_locate_reveals_its_owner ... ok +test tests::adding_bend_between_close_ports_stays_outside_owner_clearance ... ok +test tests::behavior_and_data_authoring_do_not_place_nodes_in_unrelated_architecture_views ... ok +test tests::bidirectional_transition_ui_commits_once_and_preserves_other_diagrams ... ok +test tests::counterpart_creation_focuses_visible_target_and_reuse_keeps_views ... ok +test tests::creation_from_capabilities_routes_work_to_architecture_and_folded_allocation_inserts_below_existing_work ... ok +test tests::dragging_keeps_semantic_selection_and_same_node_remains_editable ... ok +test tests::escaped_string_mode_preserves_exact_control_characters ... ok +test tests::every_demo_record_has_a_lossless_graphical_draft ... ok +test tests::explicit_relationship_builder_never_mutates_on_preview ... ok +test tests::explorer_and_creation_selection_follow_their_own_placement ... ok +test tests::fit_adapts_to_canvas_returning_after_help_but_preserves_manual_navigation ... ok +test tests::fit_uses_measured_canvas_bounds_and_preserves_them_on_open ... ok +test tests::focused_explorer_follows_reparenting_without_changing_identity ... ok +test tests::graphical_rename_keeps_aliases_and_references_and_undo ... ok +test tests::identity_property_change_is_atomic_and_rewrites_all_live_references ... ok +test tests::invalid_numeric_draft_survives_navigation_save_and_help ... ok +test tests::invalid_save_and_continue_returns_to_repairable_draft ... ok +test tests::malformed_source_preserves_model_and_is_recoverable ... ok +test tests::manual_trace_requires_target_and_selection_normalizes_creation_kind ... ok +test tests::mission_capability_authoring_is_contextual_atomic_and_text_equivalent ... ok +test tests::modal_owns_background_commands_and_shortcuts ... ok +test tests::nested_allocation_insertion_reflows_sibling_subtrees_and_preserves_fold_state ... ok +test tests::opening_and_creating_models_preserve_the_actual_window_size_for_fit ... ok +test tests::opening_legacy_source_surfaces_report_without_modifying_original_bytes ... ok +test tests::pending_source_prevents_property_draft_collision_and_preserves_both_if_present ... ok +test tests::placement_identity_edit_then_drag_and_diagram_identity_then_arrange ... ok +test tests::placement_relocation_follows_cross_layer_traceability_view ... ok +test tests::property_enum_and_optional_reference_roundtrip ... ok +test tests::reopening_canvas_for_a_modal_preserves_viewport_after_route_edits ... ok +test tests::revealing_an_implicit_function_port_unfolds_its_visible_architecture_owner ... ok +test tests::routing_actions_are_atomic_guarded_and_fit_the_authored_detour ... ok +test tests::selecting_current_constructors_preserves_payloads ... ok +test tests::source_find_navigates_an_invalid_draft_without_applying_or_losing_it ... ok +test tests::source_navigation_waits_for_editor_layout_and_cancels_on_edit ... ok +test tests::system_only_models_open_the_populated_perspective ... ok +test tests::transition_resolutions_belong_to_the_effective_target_and_kind ... ok +test view_copy::tests::duplicate_retains_ports_routes_options_and_artifact_claims_with_exact_undo_redo ... ok +test view_copy::tests::duplicate_state_view_preserves_machine_nesting_and_collapse_with_unique_aliases ... ok +test view_copy::tests::missing_view_does_not_propose_an_empty_replacement ... ok +test views::traceability::tests::traceability_browsing_retains_invalid_property_draft_and_existing_select_guard ... ok +test views::traceability::tests::traceability_filter_reaches_artifacts_after_68_traces_and_searches_all_metadata ... ok +test workflow::communication::tests::communication_requires_two_explicit_ports_and_recovers_the_partial_choice ... ok +test workflow::port_authoring_tests::boundary_picker_matches_exact_commands_across_contracts_and_ownership ... ok +test workflow::port_authoring_tests::boundary_picker_recomputes_for_unoriented_reverse_endpoints_without_relaxing_contracts ... ok +test workflow::port_authoring_tests::duplicate_port_names_show_owner_contract_and_unique_alias ... ok +test workflow::port_authoring_tests::fresh_functions_are_actual_picker_targets_and_plan_enables_apply ... ok +test workflow::port_authoring_tests::generic_component_port_connection_keeps_its_endpoint_and_direction ... ok +test workflow::port_authoring_tests::ordinary_standard_connections_default_unoriented_and_new_roles_exclude_legacy ... ok +test workflow::recovery::tests::incomplete_relationship_and_trace_recover_raw_fields_and_reject_changed_model ... ok +test workflow::recovery::tests::transition_recovery_replans_typed_request_without_persisted_commands ... ok +test workflow::transition_tests::basic_transition_boundary_choice_is_required_without_an_inferred_system ... ok +test workflow::transition_tests::basic_transition_creates_overview_when_existing_views_cannot_show_result ... ok +test workflow::transition_tests::basic_transition_explicit_destination_appends_below_existing_bounds_and_is_one_source_edit ... ok +test workflow::transition_tests::basic_transition_invalid_explicit_destination_retains_choice_and_refuses ... ok +test workflow::transition_tests::basic_transition_reconcile_keeps_existing_selected_view_placement_and_route_exact ... ok +test workflow::transition_tests::basic_transition_skips_lowest_uuid_state_view_and_preserves_oa ... ok + +test result: ok. 729 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 257.42s + diff --git a/harness/experiments/canvas-union-windows-20260911/full-ff01754.result.json b/harness/experiments/canvas-union-windows-20260911/full-ff01754.result.json new file mode 100644 index 00000000..cea120cb --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/full-ff01754.result.json @@ -0,0 +1,7 @@ +{ + "source": "ff0175488ac57a92d401b00a0e4542582a82d8e9", + "start": "2026-09-11T17:56:02.6244756Z", + "end": "2026-09-11T18:00:44.2553735Z", + "exit": 0, + "command": "cargo test --locked -p oa-desktop --bin arcadia-studio -- --test-threads=1" +} diff --git a/harness/experiments/canvas-union-windows-20260911/full-inputs-ff01754.json b/harness/experiments/canvas-union-windows-20260911/full-inputs-ff01754.json new file mode 100644 index 00000000..b705f43e --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/full-inputs-ff01754.json @@ -0,0 +1,2595 @@ +{ + "source": "ff0175488ac57a92d401b00a0e4542582a82d8e9", + "inputs": [ + { + "path": "Cargo.lock", + "bytes": 119973, + "sha256": "065c23c94908fe499d32ed91d55a70946ed5b08ecfda3b483ec1e635572257bf" + }, + { + "path": "Cargo.toml", + "bytes": 400, + "sha256": "369f5b4b2b60705a94623c8c0b68f8e0b048f263280ce07ae0629ad353797d62" + }, + { + "path": "build/windows.manifest", + "bytes": 354, + "sha256": "9c19c5a02f858ece84c44839ad2b145d9c1d90442da0b15c87fd045f975974eb" + }, + { + "path": "build/windows.rc", + "bytes": 24, + "sha256": "a27385edcae0bcf96f55361056cb7be5f36d76fcddcaefd20647c91959563e1e" + }, + { + "path": "build/windows_manifest.rs", + "bytes": 1460, + "sha256": "168da2c42ecccfebc11ee3e1b455e653a68fe9c6d30a9842a8b9e7441d086c80" + }, + { + "path": "crates/oa-cli/Cargo.toml", + "bytes": 279, + "sha256": "49c567f1af74a826683b4d96845feb3806fd9bcb7dc2767e16b5509d4e5e1cde" + }, + { + "path": "crates/oa-cli/src/lib.rs", + "bytes": 15498, + "sha256": "97bebab63b4a611a1d36ad0c20fe452cbe1985fe3a6fbd39c954603da11410fc" + }, + { + "path": "crates/oa-cli/src/main.rs", + "bytes": 2984, + "sha256": "de964f9b6c59c0f31c080afbd3b31d442b07b1b44f6a187a248d3409c45d35fb" + }, + { + "path": "crates/oa-cli/tests/source_cli.rs", + "bytes": 16600, + "sha256": "a3a1b856c7a9fd3d80bc8074150a24fbb0ef0e9ddf736c7cea036725fc3dc2b8" + }, + { + "path": "crates/oa-core/Cargo.toml", + "bytes": 408, + "sha256": "6b5c062474ebb62d5082e07dabed99ee85e4a833edf26690fb4f887028d3416f" + }, + { + "path": "crates/oa-core/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-core/examples/generate_demo.rs", + "bytes": 795, + "sha256": "e26f432c95331aeca50985dbb3ad2c558affd43023b585811f0f7bfaa59f6661" + }, + { + "path": "crates/oa-core/examples/generate_release1.rs", + "bytes": 1798, + "sha256": "1d5f11165d74893e4bc18dbf3a2f2111f4026bd89fd06ee7f0bae69e45c4d4b5" + }, + { + "path": "crates/oa-core/examples/release_dense_views.rs", + "bytes": 14664, + "sha256": "a83158488ef9b7394f136c52b1496719be935c4e5bdfe457cab5c9894f5f9412" + }, + { + "path": "crates/oa-core/examples/routing_workshop.rs", + "bytes": 17248, + "sha256": "571307b18b6d4fa04700c5a4d4b0c2154c419e93a21292b2028e7b6f46a2faa7" + }, + { + "path": "crates/oa-core/src/analysis_numeric.rs", + "bytes": 3735, + "sha256": "6c88eef3119162aeb6c07b9e75812c3af9fbfe75554b0503ad00f103cb478408" + }, + { + "path": "crates/oa-core/src/analysis_numeric/dimension.rs", + "bytes": 3045, + "sha256": "d4ad8e5307a1620e31791d724a0e31fdba860d00dc83e3e1f2209d9c5e22a340" + }, + { + "path": "crates/oa-core/src/analysis_numeric/exact.rs", + "bytes": 17338, + "sha256": "4eb8cb0f66b10e5da204899af82ee26adfec74bab47b1a6c9842b5488ee04c4d" + }, + { + "path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json", + "bytes": 6085, + "sha256": "3b959dec5e100a6f03817f5314bef497347ba579ebd235476516d4bd83399181" + }, + { + "path": "crates/oa-core/src/analysis_numeric/meter.rs", + "bytes": 7129, + "sha256": "1f7174beb992935b4c399acdef91114af73faf041070feab968455ac827e8ee4" + }, + { + "path": "crates/oa-core/src/analysis_numeric/quantity.rs", + "bytes": 12257, + "sha256": "ead5142d6eeea8f99399d57e02d8f7d819980923de76387cd454694d6a747aee" + }, + { + "path": "crates/oa-core/src/analysis_numeric/tests.rs", + "bytes": 27486, + "sha256": "323ce82fc057d36f44463569fd8ea324c7d700fddfbf32bce15e04bd2a66970b" + }, + { + "path": "crates/oa-core/src/data.rs", + "bytes": 42696, + "sha256": "7f9c7c7dd8353cb5a38597b92cde6d3e890203d392473187eb387d41ff37414a" + }, + { + "path": "crates/oa-core/src/demo.rs", + "bytes": 20657, + "sha256": "ad62123f96c20ff023cafc1a9849b0fa06bc62edd1fb9701799e4599b458600c" + }, + { + "path": "crates/oa-core/src/language.rs", + "bytes": 99938, + "sha256": "638f08a42e9cf4996b5d07f69b4d6f517a518369f3604d25be1006e40e080765" + }, + { + "path": "crates/oa-core/src/language/semantic_source.rs", + "bytes": 7326, + "sha256": "584b5bb59d87a7b8d44d5dd2d124c3a1323b21492b138a17bc9526c0087c9e29" + }, + { + "path": "crates/oa-core/src/language_nesting_tests.rs", + "bytes": 20639, + "sha256": "bd061611e8cb6f28ab203c73e544ac98bf275eb464cd314ad5fbd25f773df33b" + }, + { + "path": "crates/oa-core/src/lib.rs", + "bytes": 549, + "sha256": "ea1d9bf5acba8381ba204db12ff5b5fe1f320417d17af517e46b90fd6bcb241c" + }, + { + "path": "crates/oa-core/src/migration_v2/README.md", + "bytes": 1515, + "sha256": "fb61d3faa33d7bdfba73415415a0882c0fbd3e0b45df42f972a19aec6da927b9" + }, + { + "path": "crates/oa-core/src/migration_v2/adapter.rs", + "bytes": 9490, + "sha256": "66741b4c8354a23b8a932ecc5bf1bc3bb6e74279558757b8c35bc2157a7e51b5" + }, + { + "path": "crates/oa-core/src/migration_v2/mod.rs", + "bytes": 1158, + "sha256": "857d8bb13b6814daba6780ca770c16352943c01cb130245d055f11b3b957b3e9" + }, + { + "path": "crates/oa-core/src/migration_v2/model.rs", + "bytes": 52059, + "sha256": "8ac63eb59b2e57bdecff2477ee0634096090924d3dd1851e1c454880a889dacb" + }, + { + "path": "crates/oa-core/src/migration_v2/references.rs", + "bytes": 13369, + "sha256": "33a6819abc83139fdaebeebd1b553238a328637106cce7dd5a535f1791d43e20" + }, + { + "path": "crates/oa-core/src/migration_v2/source.rs", + "bytes": 21946, + "sha256": "74fee9b24e7c912182c998ab9d9aeec125e2009c3214959229e844c67afe2bf6" + }, + { + "path": "crates/oa-core/src/migration_v2/state_validation.rs", + "bytes": 15769, + "sha256": "e29ef071bab0a68460ddf55c46131c6f66c022d52c3c602c37c013c3649da1b0" + }, + { + "path": "crates/oa-core/src/migration_v2/transitions.rs", + "bytes": 704, + "sha256": "f74174c532ba56b57c80b719a11fb4adea0a78ccc2828fff72fe2b9eda5a10b1" + }, + { + "path": "crates/oa-core/src/migration_v2/validation.rs", + "bytes": 98736, + "sha256": "6d5e4a4e2805ae5045bf789a0485e14a61c63346587bace2b4a91173a688f780" + }, + { + "path": "crates/oa-core/src/migration_v3/README.md", + "bytes": 2128, + "sha256": "3755a63f840d7f0eadf68afa7a9d587877a9468ff982d3610539e8a0bb067a82" + }, + { + "path": "crates/oa-core/src/migration_v3/mod.rs", + "bytes": 23023, + "sha256": "f4edb6a0b50ecfe8670973776d703047b064420cb1f05913129c00fc656b3722" + }, + { + "path": "crates/oa-core/src/migration_v4.rs", + "bytes": 4020, + "sha256": "50c652964f0f309e7f3f54b6ef12c40d42fcecc0490f005995c713df1179af70" + }, + { + "path": "crates/oa-core/src/model.rs", + "bytes": 66352, + "sha256": "4585c895c64c4a183941c92cf047a9e52ff0cbe41971e16e78d229ea9dc101fb" + }, + { + "path": "crates/oa-core/src/operations.rs", + "bytes": 11405, + "sha256": "a5ffbbf54cd440316a106d1188bfb699b57dae59f466edcb33618c333cd19f41" + }, + { + "path": "crates/oa-core/src/persistence.rs", + "bytes": 31774, + "sha256": "1c01474f906a63e552c0d590b272c95821c00a100b3e79e58e67dbbeac341a14" + }, + { + "path": "crates/oa-core/src/ports.rs", + "bytes": 45413, + "sha256": "5cac12eea70a54ebebbd6be77b63ce92e3195325c4bc7d27705c518686cbac80" + }, + { + "path": "crates/oa-core/src/projection.rs", + "bytes": 21374, + "sha256": "69ed12bace2cc5024e151752a174ab14034ac2d05eb741fcf13e9a27521c059b" + }, + { + "path": "crates/oa-core/src/projection/port_layout.rs", + "bytes": 13474, + "sha256": "a08d17d10a58bb8b206dd001b19b91d5aa3a9f743b298a4da57d503223c20079" + }, + { + "path": "crates/oa-core/src/projection/state.rs", + "bytes": 5233, + "sha256": "73a607bac713ec7ff49a509fefb1a53c7686dca93ee241ffc835564d743afb28" + }, + { + "path": "crates/oa-core/src/references.rs", + "bytes": 21957, + "sha256": "93553c234f32da35f1f41a7b83ad81c471c9803efa78ca222cf7de3c2f86c0bb" + }, + { + "path": "crates/oa-core/src/routing.rs", + "bytes": 50309, + "sha256": "c5ebb350ba76f731f5c48d968bfbba4b6ef68ec046eb5893d116c4637ef15080" + }, + { + "path": "crates/oa-core/src/semantic.rs", + "bytes": 540, + "sha256": "f9895dd5b8fb27d10d4cfbb41c3542ef2018365a589b8d5778e5674ae5a1f239" + }, + { + "path": "crates/oa-core/src/semantic/catalog.rs", + "bytes": 49407, + "sha256": "1ff74fcca18084d178681d7ff38ee1ccac632c27c4de0595ba1ceab8067ab3a4" + }, + { + "path": "crates/oa-core/src/semantic/historical.rs", + "bytes": 15271, + "sha256": "eaf3284bf234312698514cdb636bc8348e1b5d746cb16b18df777e77d312abfb" + }, + { + "path": "crates/oa-core/src/semantic/linked.rs", + "bytes": 6035, + "sha256": "6f335b14e809aecfc35defcaf77cb821b080d1506dbc46297912b25af98c07a2" + }, + { + "path": "crates/oa-core/src/semantic/mutate.rs", + "bytes": 22683, + "sha256": "dc2b4c4a6eb156fe468eaa8c85fd37a505c0cd8ea1f226cb69cd01689afd3d9d" + }, + { + "path": "crates/oa-core/src/semantic/review.rs", + "bytes": 51116, + "sha256": "0c157d2c327e72e89cca3b423e71ee73798105c5bb7271439c1a101642c91e3e" + }, + { + "path": "crates/oa-core/src/semantic/types.rs", + "bytes": 10405, + "sha256": "6922fe35089f12fad43ab0ce1c6886d01a3855f90b8e2543e99802ac5e6f1bbb" + }, + { + "path": "crates/oa-core/src/session.rs", + "bytes": 35237, + "sha256": "1eb77b329b2986fc6f5dc0171e77fd860375cdfd8c769a1367b445d6c49ebe00" + }, + { + "path": "crates/oa-core/src/source_files.rs", + "bytes": 74803, + "sha256": "669491dbddb8e8e6556161426dffa1a49ac1c826d31020bff0956d81a4d84683" + }, + { + "path": "crates/oa-core/src/source_project.rs", + "bytes": 61843, + "sha256": "1394370bcc5908873b830c2c11312e6e1611af41f46c4f43ca0a6bcb713bb466" + }, + { + "path": "crates/oa-core/src/source_project/source_migration.rs", + "bytes": 4700, + "sha256": "add463ae85c420f853d324b00a221f0f874164cc3f3ed6bbb07557a9ed7a8c95" + }, + { + "path": "crates/oa-core/src/source_project/source_structure.rs", + "bytes": 20225, + "sha256": "9f1f83315856a7025fe662c0fbb13452f31613224a03663150d6225d1ddc162f" + }, + { + "path": "crates/oa-core/src/state_validation.rs", + "bytes": 15665, + "sha256": "e28d7f74385e2a78763716b34491bd73a00bfeccc085970ceb130627e3faee18" + }, + { + "path": "crates/oa-core/src/topology_context.rs", + "bytes": 13734, + "sha256": "16de69b297de95d93c73808621607833b713af811dd082f0169186ae1e5c3e2d" + }, + { + "path": "crates/oa-core/src/topology_context/analysis.rs", + "bytes": 20036, + "sha256": "dead6194f0aa60cc46045c0e5ee10dbdbb689158c38634dfbbbc45d2ba88ac65" + }, + { + "path": "crates/oa-core/src/topology_context/analysis_tests.rs", + "bytes": 5258, + "sha256": "17839263410815e96fe027d4eba3aae0a9ab3b82b16947e2b29ebe0d09782b56" + }, + { + "path": "crates/oa-core/src/transitions.rs", + "bytes": 51998, + "sha256": "fadd1b07b89cae4117b91abb4f1ac9ea81ff818ef20f2175e76ee8cf21fb708d" + }, + { + "path": "crates/oa-core/src/transitions/communication.rs", + "bytes": 8634, + "sha256": "2b476630a88edd9021b9156ca12a5402e01604095f0d4002afb452671903a028" + }, + { + "path": "crates/oa-core/src/validation.rs", + "bytes": 112792, + "sha256": "7c1b4ae5e4ebba13e99af9158ccde138dfc5e9a7afb580e76fba72d0b64ba74e" + }, + { + "path": "crates/oa-core/src/validation/coverage.rs", + "bytes": 6478, + "sha256": "b08e4b344ecad055714168c83beff7fe29ba4edc53edbffa2eae04e5580a4d5f" + }, + { + "path": "crates/oa-core/src/view_options.rs", + "bytes": 24481, + "sha256": "421ede0a04c593ca1df0e6e0d06fabcfcc8113cdbcfb0adc7f5c8ef4d1724208" + }, + { + "path": "crates/oa-core/src/view_options_tests.rs", + "bytes": 21433, + "sha256": "64aac44de2df42a1012239a3dd75c473daf0c8990b3170b7bf7e78a556080208" + }, + { + "path": "crates/oa-core/tests/communication_boundary_scenarios.rs", + "bytes": 8816, + "sha256": "e1100b8f363910a487d90c73acc7df543dc6d532158c3c63ad11cd3d40d8e12b" + }, + { + "path": "crates/oa-core/tests/communication_means.rs", + "bytes": 18102, + "sha256": "a25f2749180bc6f4beb1445bfad3351eedf0669131a7439b633ab804439e95a4" + }, + { + "path": "crates/oa-core/tests/coverage_diagnostics.rs", + "bytes": 20527, + "sha256": "e2e9c3996daab47f2bd23f5d8c48b4e372af33331152f5fc85b76f06d04fcdf4" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia", + "bytes": 29220, + "sha256": "f0df129c1914bfc01d0b047b0cd4b520d8cf0c249ea9ced085de400c0be87ff3" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json", + "bytes": 43990, + "sha256": "3ed53508eb544bbd788832f7bebea2ec31b3d66191f24f45098cc9750ec4c005" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia", + "bytes": 2450, + "sha256": "305f19d1b79f99f8d4cf627ba2e9ca6836bb7959cc0070227815937a699eafea" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.json", + "bytes": 8240, + "sha256": "1b41299c53204e6232397d25a58d5bccdbbc16e770c75e8edb55ce36ccfe58a2" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia", + "bytes": 1222, + "sha256": "5fa53a26a5ad010c8e59eb56d9b2b52003569e66c0377ffb58840590168ad0c1" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json", + "bytes": 1734, + "sha256": "2808088afcc165f4bd6da5129151143411c997041c3de61d600ab76e05d24a47" + }, + { + "path": "crates/oa-core/tests/language.rs", + "bytes": 56482, + "sha256": "77f6cb2a69976bfb5f60b243b71c07fb0edc7505002c1e9e73fcd6ddf6d9496e" + }, + { + "path": "crates/oa-core/tests/port_anchors.rs", + "bytes": 26893, + "sha256": "efbc0fa18733b1aaac501f9785a4c392fc49c05c45a5778d26e9b5dd951ca6fa" + }, + { + "path": "crates/oa-core/tests/port_migration.rs", + "bytes": 15620, + "sha256": "1801c376a5435ac7efb2a8342348b2c7ffef930bba65deca4acb66b06e0483d7" + }, + { + "path": "crates/oa-core/tests/port_source.rs", + "bytes": 4858, + "sha256": "8d2ddaca4045d330f6eda2591561497b858aad53769a823249d0bea4f5916e12" + }, + { + "path": "crates/oa-core/tests/prepared_commands.rs", + "bytes": 9555, + "sha256": "52cab1c668064777412df0a6a3c20d74d579b33794b4b00525b9f151c06eac9b" + }, + { + "path": "crates/oa-core/tests/process_diagnostics.rs", + "bytes": 1104, + "sha256": "1dbbc590239ef7542be4a2bdc9543aaa64d108417b03dd312a0894e364745906" + }, + { + "path": "crates/oa-core/tests/r3_constructor_ack.rs", + "bytes": 24163, + "sha256": "e5c44676ea0aab97f2e091ddac2f4bc4a4a90f5877f7bbf08b61229600944dd0" + }, + { + "path": "crates/oa-core/tests/r3_migration.rs", + "bytes": 6590, + "sha256": "ebd04120f5215b3956fdc79ee78f6e38734c7d13392f1fc28aaba957912265bd" + }, + { + "path": "crates/oa-core/tests/r3_semantic.rs", + "bytes": 77473, + "sha256": "49c3763eaf2131070a7c46cc23a84f917d292d41610b6404510e5607f8890c32" + }, + { + "path": "crates/oa-core/tests/r3_source.rs", + "bytes": 15311, + "sha256": "f31f60659df3c7ef601650d1cff140589d8729f01a3f65a4a6a6c2272f15c408" + }, + { + "path": "crates/oa-core/tests/r6_data.rs", + "bytes": 41437, + "sha256": "5f7ffb3bc73c3c85423623fe56f8680646dc97040327bdbead9d47a37e80f155" + }, + { + "path": "crates/oa-core/tests/r6_migration.rs", + "bytes": 9174, + "sha256": "81519bc1512f5e069432e9a1cdad04c10216d492df01b4b75b02102480ebd6a1" + }, + { + "path": "crates/oa-core/tests/r6_source.rs", + "bytes": 9519, + "sha256": "88a0aa087a7678e2898ff6057538072ca92072190712f05fff08372cc56ebed6" + }, + { + "path": "crates/oa-core/tests/release1_teaching.rs", + "bytes": 10010, + "sha256": "be179290d871125dc97cfd2a3067c7ac89c8a538fdb0afafc5b825bcdfb2bef2" + }, + { + "path": "crates/oa-core/tests/release_capabilities.rs", + "bytes": 19422, + "sha256": "b92cba2a562e78287ddd7dc3e94557848d17faa0c8a102601b50286b9af48a30" + }, + { + "path": "crates/oa-core/tests/release_diagrams.rs", + "bytes": 7689, + "sha256": "cefcc0582d91266d5de6ee5daefcfe41b606ab971ab235b59034a8e1dbc24b7d" + }, + { + "path": "crates/oa-core/tests/release_language_limits.rs", + "bytes": 5204, + "sha256": "8578da4f76bbd2d0e1bfe295ab1259e2162a3fe3d0d2ba8901915d7a40942806" + }, + { + "path": "crates/oa-core/tests/release_ports.rs", + "bytes": 32740, + "sha256": "506e62beab0c7ec0cece83e9e0c1a810fa1f99285c9e1cab0c77f06de55a40e1" + }, + { + "path": "crates/oa-core/tests/release_routing.rs", + "bytes": 23165, + "sha256": "ae168faaba23769b7ca1fd65f217b9db671726e67276eb5a04e5e72b1447a462" + }, + { + "path": "crates/oa-core/tests/release_scenarios.rs", + "bytes": 41067, + "sha256": "0f5d876badeec653fc7b12610a77d867e9a53582e605253deff8d379da21572f" + }, + { + "path": "crates/oa-core/tests/release_states.rs", + "bytes": 26873, + "sha256": "b1a26acee95eaba7f9920f9da738a1e33606552eb078e6d298895daff7e3d811" + }, + { + "path": "crates/oa-core/tests/release_topology.rs", + "bytes": 17643, + "sha256": "c51dbe5aa5a038f9ed830daa02aa67714472179eb7fd6da5ce36dc6931c8b3a9" + }, + { + "path": "crates/oa-core/tests/review_regressions.rs", + "bytes": 21823, + "sha256": "f154e5c88d35e31512e2b07733a1327ea3e2fcfd1ef8eb23066a4e9573f387c1" + }, + { + "path": "crates/oa-core/tests/route_source.rs", + "bytes": 7270, + "sha256": "1a73fdbac0d0ae0e84d8d67b6ad398e11223958f5dca007f73757c91827decfb" + }, + { + "path": "crates/oa-core/tests/routing_geometry.rs", + "bytes": 31890, + "sha256": "2d8eea4a2bba535ed0ea6daabccdaf87b375828c404baa10518c7bd7fd6253d3" + }, + { + "path": "crates/oa-core/tests/routing_labels.rs", + "bytes": 5530, + "sha256": "dff884d799c3c5c9257210248c55767e294e3cb317c605cbd0ddc9600c216eb6" + }, + { + "path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs", + "bytes": 4640, + "sha256": "d93eaf9667f4221232168bef0fe9f069e5be1b1f23410bda5dd67010b96e5c8a" + }, + { + "path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs", + "bytes": 14957, + "sha256": "7f526d363ba05e7ac89faf0fead0796e419c34ee204edf44a1b46c78308b0970" + }, + { + "path": "crates/oa-core/tests/source6_view_contract.rs", + "bytes": 14307, + "sha256": "fa566de53946d40bbbd9033edfeed177180c4dc44afa2f2f4667ffdc622eb3bb" + }, + { + "path": "crates/oa-core/tests/source7_contract.rs", + "bytes": 43262, + "sha256": "cad4f1a135ddbd947bdf3c71d1b44f1e438725095ea8c4d893464bc721c2363f" + }, + { + "path": "crates/oa-core/tests/source_files.rs", + "bytes": 13168, + "sha256": "f0429a0b89a443458ea9af7083c1e4e7411417a7e6b47e618bfbb9266dc164f9" + }, + { + "path": "crates/oa-core/tests/source_identity_updates.rs", + "bytes": 5139, + "sha256": "f52aa38b4d317e532c641ad7463472e4b40e802e60052cf0017556ffc63da06c" + }, + { + "path": "crates/oa-core/tests/source_nested_updates.rs", + "bytes": 16892, + "sha256": "9ac3390936c0d250651ea94a1b8faf0adfb680f061ececc56f2a52909b5ba855" + }, + { + "path": "crates/oa-core/tests/source_project.rs", + "bytes": 45910, + "sha256": "cef58adb16aa4d2527ddc0628aec3e26f0b42b4be70014a7e360fafdac13a942" + }, + { + "path": "crates/oa-core/tests/state_diagrams.rs", + "bytes": 23973, + "sha256": "6b02b387a3ae559cb3ad72de24292a5abcb76aaac0adbab5d2aa5211b22cd2e5" + }, + { + "path": "crates/oa-core/tests/workflows.rs", + "bytes": 33426, + "sha256": "bd843dfddbd5724ec08f657571f887496203056fe2fa5864ef50a168a613b89b" + }, + { + "path": "crates/oa-desktop/Cargo.toml", + "bytes": 842, + "sha256": "f3547b2b924d5db6706c0a8e069014b440ff65315d82fc8aced0360af4b3cdc9" + }, + { + "path": "crates/oa-desktop/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "aa2e4853b25bb7e185b1de49eda42028523c8ebf9599bfe6b7cb2a2b2a688090" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring.rs", + "bytes": 27835, + "sha256": "37c7db33e99dd4adcc5a51f09ee4878af94fd32413359890a541690080885f65" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/palette.rs", + "bytes": 8887, + "sha256": "8f9937cb30b48b83759fd084a8a5990a2daa3920280f1210abe048560241985a" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/tests.rs", + "bytes": 14732, + "sha256": "ed864839940a1910c97537b6ee151b57ab115ece6f60bae602552b84f9af4a5d" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring_tests.rs", + "bytes": 18599, + "sha256": "77eb1a0038d038b989f7ac568760e16fe23c4b8d73cd9fbe32b70817ea23cfec" + }, + { + "path": "crates/oa-desktop/src/app/behavior_lifecycle_tests.rs", + "bytes": 13669, + "sha256": "1af2e290aa259a6af6b0585950cda93fe3a562c7b8be1e938fe1d80747b11808" + }, + { + "path": "crates/oa-desktop/src/app/checkpoints.rs", + "bytes": 29576, + "sha256": "55e5babdcabef767af3bc8be9cc3153b3f5cc0695701b890820ec0225643852e" + }, + { + "path": "crates/oa-desktop/src/app/diagram_selection_tests.rs", + "bytes": 5582, + "sha256": "4fa507adbc23db3a6eb202d809c695ec0ebd6181356930c4fa7528a9a0c7be57" + }, + { + "path": "crates/oa-desktop/src/app/hosted.rs", + "bytes": 25059, + "sha256": "2f7094c078e1d7cbccd6c0d310afa6c400a7e031f12918bbfce65ba62754e316" + }, + { + "path": "crates/oa-desktop/src/app/hosted/tests.rs", + "bytes": 30344, + "sha256": "fa118f64d04b400ccc01e4e67adaf3541975288882afa265a9e7f092cdbda232" + }, + { + "path": "crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53d4a422d3066e07cccb9e8ef2ad08ea95e451b9b7cbef29a27e6634e035ed97" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests.rs", + "bytes": 20067, + "sha256": "b96ddc6bff7f8fde97720e18a92070f891c1340348d2e19c7c9c5a7bda650ce8" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests/mounted.rs", + "bytes": 32805, + "sha256": "14a4914a1c86e50c395672dab8fe80d7af679a79c9082890b509fad7b7c543d9" + }, + { + "path": "crates/oa-desktop/src/app/review_navigation_tests.rs", + "bytes": 7049, + "sha256": "872fa91479dad5436e139ffcaece270323e9f5daeeb7a4d80c2c2534f48bc21b" + }, + { + "path": "crates/oa-desktop/src/app/save_as_tests.rs", + "bytes": 4113, + "sha256": "f8b915793f1a36fd152569c4781a4bdc17f4af404a8ceb373330053413087a1c" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram.rs", + "bytes": 9979, + "sha256": "f7fd50cc9a86adb794713958348af63afc7c698f74965e13188b7b8a9e735c55" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram_tests.rs", + "bytes": 39957, + "sha256": "612e414b1a2cab12cbfd8fce23745559ff27577700694d53af67ad1f2883ee2c" + }, + { + "path": "crates/oa-desktop/src/app/transition_context_tests.rs", + "bytes": 11508, + "sha256": "47002fb025f3a2f8c53871f9509bfe8611a61db6fb0e1b3125eb6e875425cb1d" + }, + { + "path": "crates/oa-desktop/src/app/view_copy_tests.rs", + "bytes": 7846, + "sha256": "296821c51dcde568e7d717039aa26bfc1b56a9074aa16051007bb351b33a4d44" + }, + { + "path": "crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5e7cecb54bfca64e4462f7881f460900abd925ba2cfe9451ce2b0c78af4bc850" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7758, + "sha256": "9303ed4f74ee0317e4abeab134f7c31f50eac5eb5d85940d771cd3ad595258fa" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7860, + "sha256": "851af1e9b155d6d0d079d9164c5616fadc33e961391a6604a43eabf5158957b3" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "cc58cb3838f546150d22fe4dd83a3ffd52504c279febaeb8d0b4a6a3b8a73950" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 27020, + "sha256": "d84d022574a6f1d69f203d9383502227fb9fc23605c6fb48779ca9f09bee546d" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "af1c7c1e802d1207ee6c7bd66b703811a250d7b56511512af6c0945c776e68a7" + }, + { + "path": "crates/oa-desktop/src/behavior/contextual_source_tests.rs", + "bytes": 49386, + "sha256": "5f05318a6533503b9d6febf1ec4dd7256f192394bc1b952a7904c8175bd8ddae" + }, + { + "path": "crates/oa-desktop/src/behavior/creation.rs", + "bytes": 28978, + "sha256": "33ae9aaf4ba90de9432870cc8e99708ef39e3638ae8283a9253c333785c91299" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/app_tests.rs", + "bytes": 13181, + "sha256": "e9c5bf2b07357b7a2f679ae0d8f6904c1c1461aaeb94fa0db478b54d4641ae3e" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/tests.rs", + "bytes": 22793, + "sha256": "a0d1daf9fb3920f7e6181a9802d84fbe056cbf0043b9e41dba5fdda687d9818a" + }, + { + "path": "crates/oa-desktop/src/behavior/form_session.rs", + "bytes": 3941, + "sha256": "f5027fb3a5a5599a182e7d1a10dba56dd66eb1f23a4ba4c4a1744c9a6a5b6e61" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "fb1da03e8df05a59253f7fdf34391e30abd8aea427b7d0a12b86b80ff3bf9eca" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/focus.rs", + "bytes": 9728, + "sha256": "73a9ea10ad7312086081b8688396a21ab4251c9faecc2df6d984b702f3963533" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284f5a7da09bdc77d70c6788a091d57927150c796ffc10d786793cff7da4b7c6" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950cf4347a3edea615b1ac71d9a681c32b446b5109fcfd0f03f3ae2b147360bf" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle.rs", + "bytes": 9324, + "sha256": "758d244c2e8de61f515d540baa4d967275f49720e0253c6c9159eb9acd71c859" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle/tests.rs", + "bytes": 15430, + "sha256": "01b576993b329810cd78a859b45ac52c66f5a14549065661dd1308196e0b724d" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_operation_tests.rs", + "bytes": 10087, + "sha256": "89fb71c690e07eac314f7a5e05862d5c1e2f9c21039673bf22c4ac894d21c977" + }, + { + "path": "crates/oa-desktop/src/behavior/source_draft.rs", + "bytes": 18031, + "sha256": "08190823090bd8b0305809099abadcc0de431175f7044f1d5abdceda52612ee1" + }, + { + "path": "crates/oa-desktop/src/behavior/state_canvas.rs", + "bytes": 93561, + "sha256": "2e2ad57b86f4cc1cc0c4c4c230571ef19bcc2f052001a8bdcc44925e5103077a" + }, + { + "path": "crates/oa-desktop/src/behavior/state_edit.rs", + "bytes": 35736, + "sha256": "cb2289e727163626152fc2f11195c6ee824851cb9442c57496da97fe7aebfb7c" + }, + { + "path": "crates/oa-desktop/src/behavior/state_menu.rs", + "bytes": 7447, + "sha256": "cc87c5caa177e114e4d9a7f7367e37ec2b0d1e1c16b0b99709b05ea0e59cf894" + }, + { + "path": "crates/oa-desktop/src/behavior/state_view.rs", + "bytes": 6988, + "sha256": "1eb892a9139ecb0671784c879ee5114f27e598e5b3589f5cdad0abffd5909f24" + }, + { + "path": "crates/oa-desktop/src/behavior/states.rs", + "bytes": 26986, + "sha256": "8986ed9492960e20f1254e9dfe3ff91d79ce9fef66f3bda738f9933278043954" + }, + { + "path": "crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723e51a7da4eb224d51c1659ebbfa4dc8b11557bc8bf70f6e3e1649bad24449" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual_routing.rs", + "bytes": 16731, + "sha256": "d914b0e82aa8d253fe64be2ef74443d1fd2e6cec26d1cf30620a57646e1d426c" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309b8978ec2586ff4614236c2f624af6d934d117363e917b8c1cf9f2b1295eb" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733f8badafb9ba87e3f861f03f78809450eee72cc118341eb03b24904790144a" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "a990947cb39b2fb15c21dfbc95ee16f92bf6be749d3872c6904762b9bde37089" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "b2d502c11426bffb8763515f2a56aa4c9f8e203bb36d65eb34f81c123fc406a6" + }, + { + "path": "crates/oa-desktop/src/connection_edit.rs", + "bytes": 5301, + "sha256": "020581e077f82b43c936b623cc51733b02b88428df4e5f25df74b9705819fa37" + }, + { + "path": "crates/oa-desktop/src/connection_tool.rs", + "bytes": 27023, + "sha256": "649da93677d52dae495b0e19bc32f3122ab05a6d84de8402578ff5ce3f6ee1d3" + }, + { + "path": "crates/oa-desktop/src/context_menu.rs", + "bytes": 52955, + "sha256": "629354b34dc232763700251f2695b4d4e53d6251c0b1432ecc0dfe424f7dfeac" + }, + { + "path": "crates/oa-desktop/src/data_values.rs", + "bytes": 23877, + "sha256": "e1deb1cad551f0a9fdef9653aad257cfb36e6d9c0f93b6245061ec3fab8fd365" + }, + { + "path": "crates/oa-desktop/src/data_view.rs", + "bytes": 38871, + "sha256": "158b852be5d3db450aa5cda8b68a35bd96bddce1d8d053a7a23582095c06362d" + }, + { + "path": "crates/oa-desktop/src/diagram.rs", + "bytes": 105256, + "sha256": "4580b170e264d14e5cfa874709f6d2e076a05155bfbe1809336a9d05f75e98ef" + }, + { + "path": "crates/oa-desktop/src/diagram/lifecycle_tests.rs", + "bytes": 9316, + "sha256": "d0e72cc0574657d5a31c3ce307b58401d0ed77eedccd1aa02377f55ab0bc03f0" + }, + { + "path": "crates/oa-desktop/src/diagram/pointer_tests.rs", + "bytes": 11833, + "sha256": "10ca5049034159205ac8f22805354f308b0ba0596f331568c26cd1393bc8cedc" + }, + { + "path": "crates/oa-desktop/src/diagram/port_hit_tests.rs", + "bytes": 5624, + "sha256": "d2d0647629bf725b4dddfc211f5a27d495ed093a79af87cdae6c196f09a97b93" + }, + { + "path": "crates/oa-desktop/src/diagram/port_labels.rs", + "bytes": 13408, + "sha256": "28782983b82e4fca5dd4410ac8bd562359943b7010c7b24ecc42f0c7ad7fb63e" + }, + { + "path": "crates/oa-desktop/src/diagram/view_options_tests.rs", + "bytes": 2815, + "sha256": "cf0c98a7715558baf17b59f67a80556f98ad30262b4434635e617c6601775433" + }, + { + "path": "crates/oa-desktop/src/document.rs", + "bytes": 31080, + "sha256": "2ecc2cd496d4a83fd7a32b9dc68e49f0f1cb31a06263dfd6939b4764da5b73e2" + }, + { + "path": "crates/oa-desktop/src/document_save_as_tests.rs", + "bytes": 5781, + "sha256": "83dc243a25f4e97674ff0d86c794a490cc923e98f1f9fb05bb038f622f11e20b" + }, + { + "path": "crates/oa-desktop/src/explorer.rs", + "bytes": 14764, + "sha256": "fa01c59219564ba9ae5553abc2b6f94e9849f9148d5812ef99b40318fe5dcf9a" + }, + { + "path": "crates/oa-desktop/src/explorer_view.rs", + "bytes": 13773, + "sha256": "a720d7484bcc3a24729332a00e005d1cc6f5f945a9cc3965134e0024dffb581f" + }, + { + "path": "crates/oa-desktop/src/forms.rs", + "bytes": 105544, + "sha256": "ea124f479fcd2d3978ac3cb8eb81fec4a7aae5e4f4414c4bdd54849e954dedf9" + }, + { + "path": "crates/oa-desktop/src/forms/context_fields.rs", + "bytes": 10831, + "sha256": "eaacd474f6500dd344d5a8b63ccb1d1dd86fe2aea78ab708afa8a80d62ce69bf" + }, + { + "path": "crates/oa-desktop/src/forms/data_fields.rs", + "bytes": 49285, + "sha256": "a540a0d451bff56a96b1d28643b2e8c0649f3846350c25c5b154729c0538e77c" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields.rs", + "bytes": 27414, + "sha256": "e7100474cde18bdfe95562bcb3c055f29b4b15aa64788a578194464aebcd046e" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields/tests.rs", + "bytes": 33950, + "sha256": "16d3c308138c063f45986d53c621ad83c7dc2ce853edd31b7204e2939a382df9" + }, + { + "path": "crates/oa-desktop/src/forms/port_fields.rs", + "bytes": 30554, + "sha256": "3fe684d651d5be6203fd9db3979c772913e1af5ac1514e67be386ffd88dc8127" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser.rs", + "bytes": 12130, + "sha256": "b60af363be333af048859e51c28e8c989cb5c95d696cad16c8eec15f78d57a12" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/adapter.rs", + "bytes": 10447, + "sha256": "87ab4bc3bf27fd4b5a458791cf62c7ba8e44e9ebf846d8b851b8b6ffb454cd64" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/tests.rs", + "bytes": 27835, + "sha256": "8e98f8a3fcb814d4a201cc85b3db87a689fd3d2ce487f8a2fe312fc296f93377" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger.rs", + "bytes": 13336, + "sha256": "34d0acecfe6bed193970aa473cc3313984971264b86d7b362a6b84c23d013c0a" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger/focus.rs", + "bytes": 9923, + "sha256": "f1e79f4b03b2f9fd1cdc335054d4ae177dc9e759b6ea3d334b2bcca39b57191b" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/widget.rs", + "bytes": 33804, + "sha256": "d66cc17af7a48bb8598be683f084bb08fa7af7304f9636f0a1bea94c5cf6c727" + }, + { + "path": "crates/oa-desktop/src/forms/view_fields.rs", + "bytes": 24987, + "sha256": "5184d438f0dfd4e29f284ff048e19da73e71be1f24a6e9f123c7e922aecb85fa" + }, + { + "path": "crates/oa-desktop/src/guide.rs", + "bytes": 113998, + "sha256": "ee57f8847ff1692ac566d19a66caa7bb77ad49b5f4e30c562b93117c59bc2671" + }, + { + "path": "crates/oa-desktop/src/highlight.rs", + "bytes": 2751, + "sha256": "da7c254fff7052e5fd9ec7469f473b91a819406028d7c304f6e22c4169561600" + }, + { + "path": "crates/oa-desktop/src/home.rs", + "bytes": 17587, + "sha256": "f03c1071334894f586a5a7c86026602a694db3c2faf902822e1dbe5156a859c2" + }, + { + "path": "crates/oa-desktop/src/home/tests.rs", + "bytes": 8167, + "sha256": "3b03ccfc97dfb75954e1e0ed568bb87a632295c07b96cedc05ed381f87b378f3" + }, + { + "path": "crates/oa-desktop/src/hosted/client.rs", + "bytes": 21912, + "sha256": "54b24b4b50f8d87d7c92946ea5a2cff4c98760e70555b3ce3d90be27115e95d4" + }, + { + "path": "crates/oa-desktop/src/hosted/client_tests.rs", + "bytes": 23009, + "sha256": "3ef70907ca75e098d631c41b90badd4cff1e1eb1b3c78e15a9f811c13ba8ffbe" + }, + { + "path": "crates/oa-desktop/src/hosted/mod.rs", + "bytes": 3744, + "sha256": "1b75e11c80fcb38fd20bb3487e3f88729fdac73e471c5ae64da28c249f77c63b" + }, + { + "path": "crates/oa-desktop/src/hosted/view.rs", + "bytes": 9281, + "sha256": "fc0c7b7ce3a5f05c49713df6661067751f81e810af11c614ca265ea9490e6463" + }, + { + "path": "crates/oa-desktop/src/main.rs", + "bytes": 1511, + "sha256": "cb0a919fd0df0924591283da5b55d64d525d8461640441509350be840235e51a" + }, + { + "path": "crates/oa-desktop/src/model_menu.rs", + "bytes": 34680, + "sha256": "12994a5433a91f8295a59d4bf40f35f1edb8d8dbb497a11eae8760299627f1d4" + }, + { + "path": "crates/oa-desktop/src/navigation.rs", + "bytes": 8839, + "sha256": "71355a12e1b4791106426d83fe5ebf0b29bce8e1dc594f0655ab85695469669e" + }, + { + "path": "crates/oa-desktop/src/port_edit.rs", + "bytes": 7809, + "sha256": "70b99f577c4fdeda8780c8dc1ee7b1639fd77aaca06658f06d725139fb3efb6d" + }, + { + "path": "crates/oa-desktop/src/port_preview.rs", + "bytes": 35855, + "sha256": "069487c4ece737d52e8ae4a84dcfb132236aca6799e841c91c8e7244320cf871" + }, + { + "path": "crates/oa-desktop/src/port_workflow.rs", + "bytes": 40582, + "sha256": "ccc15f97c3272145a67bb5857b4faeaf7437561bd61912fa27ab26e8824146a0" + }, + { + "path": "crates/oa-desktop/src/recovery.rs", + "bytes": 21041, + "sha256": "57e22e5fbb0e994e64ae03e69450271fe9f04a951db1670c2a018abe99db4489" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/README.md", + "bytes": 1587, + "sha256": "a90002df1e087d2dac7be26926864243568a0de7c4103e4927435b34178e8016" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/profiles.json", + "bytes": 473, + "sha256": "2c12ae96b50e6da5d60ce2b7fa00204be71dfbe2fd51311fc337a0de6a4332ff" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-bundle.json", + "bytes": 80974, + "sha256": "12715995e6f7f35414aea53b3ad720b9b31873ffdeaf4444231db1a5ac3c463e" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-model.json", + "bytes": 86191, + "sha256": "5535a706df9ec21715e8b6a5aa6dbd79636ae461450aee61305559016167612b" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-bundle.json", + "bytes": 85286, + "sha256": "ea88890a33f47d67dfb06d6dbbcc179058cf28dad76ae7d615c00aaf30dbbbfe" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-model.json", + "bytes": 90629, + "sha256": "5bb8db0b6394276666323483f977ac3586f88554c81a148c6d508fca0ef4576f" + }, + { + "path": "crates/oa-desktop/src/recovery/source5.rs", + "bytes": 6956, + "sha256": "0f0a763f0c98c2c908d4c2b95d8a4c042b6a255ee1cc16260e5404669691f642" + }, + { + "path": "crates/oa-desktop/src/recovery/source5_tests.rs", + "bytes": 12146, + "sha256": "cfd17a742f1f27b523fce7cd9309f0a07a30413395f6c58659fb91d02834a7a3" + }, + { + "path": "crates/oa-desktop/src/recovery/source7_tests.rs", + "bytes": 18213, + "sha256": "7ee61de66c75bf949489ba51ddc9677e2b4efc68801cf27909c9b894bfdc9288" + }, + { + "path": "crates/oa-desktop/src/route_edit.rs", + "bytes": 2941, + "sha256": "457ae59c2761a43a8620b4d336a7cf0f54b965997110ecaf968756a88b6b0374" + }, + { + "path": "crates/oa-desktop/src/semantic_review.rs", + "bytes": 3432, + "sha256": "8dd1e93783d63a663c7e56c28775a5c389cfd7a6b562013d6a95c891afa21ed8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts.rs", + "bytes": 24201, + "sha256": "a79236a8150597ff808597e59cf4f1e9e61c74c4901d9c8cf42d3427bb52a9a8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs", + "bytes": 11034, + "sha256": "3f255d04e4bffa5a467b75e3cbf6f7149aa3269a02f2a89fbaa1db0e5df9ae62" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_tests.rs", + "bytes": 20798, + "sha256": "04d0dd2ac453705185663f705282aa4a3a31433466a5be305764248da1a03c32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_view.rs", + "bytes": 6447, + "sha256": "2100ff73565765d8293c84f6ce2d8e566610c1524c50e32ce3c37dc33b6686e4" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments.rs", + "bytes": 18160, + "sha256": "de0ec0c740895b160ff5b847332c087c3f1a48931583464069470cc61315ed59" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_tests.rs", + "bytes": 29389, + "sha256": "8b26be0324e0f33b1e7cf7af340350df8ed98963002d6da8589ee0e6f6900bea" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_view.rs", + "bytes": 3032, + "sha256": "932f231554d77c7cb119d33bb06b9879db5e12127aed187e67013e77b6458e78" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch.rs", + "bytes": 34485, + "sha256": "a462eb627ae19348072775cf8e60b17d62f6b682b74cc413906d493fba1f0b3c" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch_tests.rs", + "bytes": 27546, + "sha256": "b67c4d7dcabac710cf3d30730a8db323180a558a415e2d117c868cdfa4804619" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose.rs", + "bytes": 27155, + "sha256": "c6adcc9882dde65e9f04f6eda7f0c855c5cbb285cf421123029169112793ce9b" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs", + "bytes": 9809, + "sha256": "440daf8d43278ef981c620d1818d0daaed8fb83193b8e8ff67b2e42fbf882938" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructor_tests.rs", + "bytes": 54132, + "sha256": "85149521ad7df62918f6f2fab6da2b3346c342ab4bce7100de3f6206c1e81096" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructors.rs", + "bytes": 12258, + "sha256": "0982163439b06a42d9af1643530364109282e63f05b323a35a76d8241f3caa32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/history.rs", + "bytes": 3211, + "sha256": "df038c2464eab8093a983f3104f0d04ca8f602010a50e1d3eeb8e53f2309e201" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed.rs", + "bytes": 14175, + "sha256": "bdd6c0fdc7108056bc5d7433254503449eed2209813680e717ead24b9d0ce834" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows.rs", + "bytes": 13222, + "sha256": "4b7bf25806471ad22d89253b9af8000f5acff53b4cc0aa1c1a83138e7b270eca" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs", + "bytes": 30804, + "sha256": "1a3b09e7ed100411cdf8043f9a78defc664a873ee0201117709185c201a5c57f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_tests.rs", + "bytes": 31163, + "sha256": "9dc9b989e5a9d408427ca1baf86b2454f9dd3baf40a551c70ca2359dc18a1227" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind.rs", + "bytes": 789, + "sha256": "4ed19952ea511d1b3226564a138e3322f994371d6b49794c14bc3689d1393e7f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind_tests.rs", + "bytes": 23424, + "sha256": "5fcf9930e949aebeb12eab0327264f32a2388d62dbf098bd2365711e22623c52" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered.rs", + "bytes": 9705, + "sha256": "6a1ed894d1b9d0110bc784a75c49444f1d14f1f74620d701f22203b2d8d0a813" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered_tests.rs", + "bytes": 22383, + "sha256": "5c8ea3a69e3da3b94d13f50f12aa7dc53246ccba25ffcace9552a0eb76b0bf36" + }, + { + "path": "crates/oa-desktop/src/semantic_review/recovery.rs", + "bytes": 28769, + "sha256": "88e69044d90c481593bab7cc125f0401c05202fccdde07698cec9483947fe8be" + }, + { + "path": "crates/oa-desktop/src/semantic_review/row.rs", + "bytes": 38159, + "sha256": "b08fdc503c6c4d2ff99adac7e18c9baa861bf86155e00b55d1e492430980d0f2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/scalar.rs", + "bytes": 15153, + "sha256": "9ad202174934dd9a63fbf9cba43135aead571db60846c8b7b1d3019a11df26c2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/source7_tests.rs", + "bytes": 11665, + "sha256": "5e8221d77b068fa30c1571988448cb8829711735881d810f3d333cbc95d34a9f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/tests.rs", + "bytes": 32480, + "sha256": "077ec6501dfaab0b05ea3987943b204f08352952296d313555051d998ccd232e" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view.rs", + "bytes": 57626, + "sha256": "7cc7dd213ab01dadad5debaee868c30a1099466602869a799ebdf9b680e8eaf0" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs", + "bytes": 4737, + "sha256": "6c766b29c8664d15cb113d2cc71bba9b60ae4ccf7e44c9c5d3c41e1dc4b7ed90" + }, + { + "path": "crates/oa-desktop/src/source_navigation.rs", + "bytes": 3938, + "sha256": "c1da10882f35f8eb0675e68c642c9a209dde09679cded414cfb9903a4e49ec77" + }, + { + "path": "crates/oa-desktop/src/source_tree.rs", + "bytes": 21816, + "sha256": "4d55e0110a1eb128a5d26c28f3aae5853d79b7245812b7c82bd275f38440a5aa" + }, + { + "path": "crates/oa-desktop/src/source_workspace.rs", + "bytes": 15522, + "sha256": "30f63a6b8d65a38d66061f90d864b5746cbacc0fbd65abc028a8f3071c47d358" + }, + { + "path": "crates/oa-desktop/src/style.rs", + "bytes": 3015, + "sha256": "05d61bec16141612ccafa0988e8c4827f41754509efdb3b961eab69127ded896" + }, + { + "path": "crates/oa-desktop/src/tests.rs", + "bytes": 58557, + "sha256": "b017218195eea2c96aba12eba8a99a38945c8ca910a652e272d34883bdb98691" + }, + { + "path": "crates/oa-desktop/src/view_copy.rs", + "bytes": 9043, + "sha256": "5630f31c942e1226aafd67eabdace6a5c5494875bdc6f7f077019e8110ba41a9" + }, + { + "path": "crates/oa-desktop/src/views.rs", + "bytes": 48347, + "sha256": "7148e01e8479c2294093159969bc33b9ff0cf916799d481681811c4de5c60e53" + }, + { + "path": "crates/oa-desktop/src/views/traceability.rs", + "bytes": 7501, + "sha256": "4c89dcf35c25e737d8cce4d543a5989ca104a5678e6cf40df47e457773d2ef94" + }, + { + "path": "crates/oa-desktop/src/workflow.rs", + "bytes": 82963, + "sha256": "d9e7b11a86fd3ec79d91f0c9bc9cebd5a2043cdaa7261622454fb7730ad8e060" + }, + { + "path": "crates/oa-desktop/src/workflow/communication.rs", + "bytes": 11084, + "sha256": "34d121ad97706012722e4bb6210072ecc3a5b4718b0ceab0c3d0f14d4cd780a1" + }, + { + "path": "crates/oa-desktop/src/workflow/recovery.rs", + "bytes": 9859, + "sha256": "0849b0f34dba5e8761be5fa05784d134ac74e4e9db395d6b9b7d6cff71e3f3dc" + }, + { + "path": "crates/oa-desktop/src/workflow/transition_tests.rs", + "bytes": 8553, + "sha256": "589b1b2dbadca74a52c2cfcfd4fe620bd4790abd113ac02182e91616c52d793c" + }, + { + "path": "crates/oa-server/Cargo.toml", + "bytes": 778, + "sha256": "527ff5e15010a8b560a6659aa1ae4bf0c8df0d9e019f03c47529ec6a750c0982" + }, + { + "path": "crates/oa-server/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-server/src/error.rs", + "bytes": 5610, + "sha256": "93817f1502386e4662c96ee1c7d1c4afc4e33bda6fd64e46c86c8e1afd5a9368" + }, + { + "path": "crates/oa-server/src/lib.rs", + "bytes": 7743, + "sha256": "20bef350350ed6339588b23af684abf745f97916c7edf3853ee042abe687e664" + }, + { + "path": "crates/oa-server/src/main.rs", + "bytes": 4163, + "sha256": "d59a6a2b5c7034bd2e00a516d2d49abb16391bf0316c1c357ddafb9c82d2d1e3" + }, + { + "path": "crates/oa-server/src/preparation_http_tests.rs", + "bytes": 5523, + "sha256": "454b90d0055d35644c8d990cd7576ddf675a9b271ca07fe9b0efce25d8ddbc07" + }, + { + "path": "crates/oa-server/src/routes.rs", + "bytes": 46053, + "sha256": "3f9a0c22043dbf28f7a8e587d2fc3abf869f76f4d23a46fe0aeb9c4392454a4b" + }, + { + "path": "crates/oa-server/src/store.rs", + "bytes": 25524, + "sha256": "37dcfb845b7993d5bced2bcf25fedfcd2a9c20a748f9315afe6cca1f41f89ae5" + }, + { + "path": "crates/oa-server/src/store_preparation_gate.rs", + "bytes": 4440, + "sha256": "427c390da3c0281ba1fe115a93ea90800e3b45fce6bcb9185e71982ddd885fec" + }, + { + "path": "crates/oa-server/tests/http.rs", + "bytes": 71892, + "sha256": "50daa6a90e96ad6fe3c6a27df2eaebe486270599c203892d09769c547a194975" + }, + { + "path": "crates/oa-server/tests/http/communication_limits.rs", + "bytes": 5790, + "sha256": "ba5474dd8e494a0176fffb4548c8e3deb2081b15de33bfce82394c764e0a2c16" + }, + { + "path": "crates/oa-server/tests/http/data_contracts.rs", + "bytes": 25949, + "sha256": "8cd456db83f301baca380acf025a5a99db4edf7d8839537dfbd02c4307e10e6b" + }, + { + "path": "crates/oa-server/tests/http/port_anchors.rs", + "bytes": 15086, + "sha256": "7199d0a208ddad9e1518b6e3fb390d64da111716d7505ba5c995deae19ad1258" + }, + { + "path": "crates/oa-server/tests/http/semantic_transitions.rs", + "bytes": 22832, + "sha256": "856d1c9b633ffb581e3e631f4e5a607789a02181dbf060667f208a569bfa4920" + }, + { + "path": "crates/oa-server/tests/http/source6_views.rs", + "bytes": 13023, + "sha256": "867bccda0e030eeeb9976b4ceeb47b21ab5044d700278022d5fb495bbb64ca07" + }, + { + "path": "crates/oa-server/tests/http/source7_contracts.rs", + "bytes": 11000, + "sha256": "0e0a467186da7263c0754890aff69ebd0d92d36b5224e4865bb6854b18dab7e8" + }, + { + "path": "crates/oa-server/tests/http/source_projects.rs", + "bytes": 30907, + "sha256": "8ff580f317ec267d1cd72ae14d8638f74a77cdf99ca4c78f66f4290c2d77460a" + }, + { + "path": "crates/oa-server/tests/semantic_http_contract.py", + "bytes": 7251, + "sha256": "4737ff94f10761f94151a694414c5c62737e0391ec820bca132830652bc4cca2" + }, + { + "path": "crates/oa-server/tests/semantic_schema.py", + "bytes": 25111, + "sha256": "cacca116c42ffbc097f1419c0f89921fa0dfd3faa864d46820ff744cfe6967f9" + }, + { + "path": "crates/oa-server/tests/state_representation.rs", + "bytes": 12241, + "sha256": "6686c826f28248ae4b63ec3f2112e193d4a347724b87974483217fcfee49cd61" + }, + { + "path": "docs/acceptance.md", + "bytes": 3463, + "sha256": "32ed3e4fed63f5c464e0937221f401d71623b276dd70f083531ac6ccebbf9050" + }, + { + "path": "docs/api.html", + "bytes": 72515, + "sha256": "a06126cb741da57e9c06e83fc4137ccde0de05562673546bc9d405878f51ea07" + }, + { + "path": "docs/api.rst", + "bytes": 41929, + "sha256": "ca7d310138c3b4d429d80a1d03d1ac8d4a9296069e205da573034256d10438a7" + }, + { + "path": "docs/architecture.md", + "bytes": 10974, + "sha256": "a50bd1645222ef3e574fa07a6ab9aa213431ee8833c6c0158ad91ce0f799543c" + }, + { + "path": "docs/language.html", + "bytes": 175111, + "sha256": "67dcfa3477b48a0b07298ec81741840cc3ccff12c7d58164873d746d41d611c8" + }, + { + "path": "docs/language.rst", + "bytes": 130753, + "sha256": "ac6b1d982c55f246de2f3c844b24b874599a3c20cfbd7ed22efe4561ac622b83" + }, + { + "path": "docs/linux-packaging.md", + "bytes": 8671, + "sha256": "41a1bb1095801df37a1411341c5791d55ea8c365739314d599a60269ac4bc7a0" + }, + { + "path": "docs/openapi.json", + "bytes": 346920, + "sha256": "bc5825a3182a4df5f6710a80262f4a09b4c15f0e4cda7ca74e1746ceca81a89b" + }, + { + "path": "docs/research-v2-architecture.md", + "bytes": 40445, + "sha256": "1b3809f2dabe6b1805dbf3d3f2625c471cd21356c7bd603916466a8c4ced8090" + }, + { + "path": "docs/research-v2-semantics.md", + "bytes": 31535, + "sha256": "b7ec3677300248926a88b84da3e7ffe52f5b73524c7712c927a0e951569db04f" + }, + { + "path": "docs/research-v2-service.md", + "bytes": 36209, + "sha256": "4828df5bc679e7c71f49b4b85c9e7dacbec52f46d416fed48af70e08886d4e4e" + }, + { + "path": "docs/research-v2-ui.md", + "bytes": 3245, + "sha256": "894e721b59a8ca85f0d95b6706ed5f130a0372def460cf33aeefc9e5e38cde0a" + }, + { + "path": "docs/scope.md", + "bytes": 16260, + "sha256": "58b88a7f0b1c8a79edf23c375a141d7b1b821bca4f3212a80e29db5ea3fba796" + }, + { + "path": "docs/server.md", + "bytes": 23361, + "sha256": "4eb24937d424ba8b384e1734e02a6f8e963ae11d2715889c92b14b39ff2e4a48" + }, + { + "path": "docs/source-cli.md", + "bytes": 3374, + "sha256": "34f12b72cc6d6ead4f47c7818c1897db592f8ad493c70776db2ab48c24d8f051" + }, + { + "path": "docs/v2-interface.md", + "bytes": 11807, + "sha256": "c7fbaf0ebd3fc9f6c11f818db2b0b9d68a33f580cfced22bde83ce08ae790bc7" + }, + { + "path": "docs/validation.md", + "bytes": 6505, + "sha256": "f46ea950ed1a54409aa07c4f9e90f39f8e2d00c1dd1b6cb805caa3993845967f" + }, + { + "path": "docs/windows-packaging.md", + "bytes": 10406, + "sha256": "2c852ff7041713ffbff3c951ac98df647c51fb70b0feba9000f7bfd5bc2703a8" + }, + { + "path": "examples/coastal-rescue.arcadia", + "bytes": 33107, + "sha256": "7e183ef9fd8154d3074303f1956cd0d9871c19e3ffc630d333b0bffbd6e30e17" + }, + { + "path": "examples/coastal-rescue.json", + "bytes": 49715, + "sha256": "242f0b031229a87af5c4ebe5d651defe59c191a6b7a69dc0a77f7e203411f195" + }, + { + "path": "examples/legacy-v1-coastal-rescue.arcadia", + "bytes": 3449, + "sha256": "4dbbbdf8bc7e6cb8e0a81dd678069dae585a9e5187c59bc3665e1fdab3f1a86a" + }, + { + "path": "examples/legacy-v1-coastal-rescue.oa.json", + "bytes": 5976, + "sha256": "fd0e9dedfb7adb922dc0575cc2b820d40344d639f1d8052bd70363aa59de9c24" + }, + { + "path": "examples/release1/README.md", + "bytes": 11137, + "sha256": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5" + }, + { + "path": "examples/release1/evidence/repair-check.md", + "bytes": 1419, + "sha256": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5" + }, + { + "path": "examples/release1/expected-diagnostics.json", + "bytes": 288, + "sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e" + }, + { + "path": "examples/release1/model.json", + "bytes": 134520, + "sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727" + }, + { + "path": "examples/release1/model.rs", + "bytes": 28762, + "sha256": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5" + }, + { + "path": "examples/release1/oa/activities/activity_oa_restore_work.arcadia", + "bytes": 1307, + "sha256": "83e73d0a013d5c3e1a4af0d9139ab0cfa362d25a846671fc2dac59181061a9d4" + }, + { + "path": "examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia", + "bytes": 697, + "sha256": "172c32c044ef4b5bc58a1f4635cc81f48dabb4014479b47dce7b25fedf2dbd0d" + }, + { + "path": "examples/release1/oa/behavior/process_oa_field_procedure.arcadia", + "bytes": 1781, + "sha256": "d2552dc5383e2e91cb302a790364e197f663c2edc013a2ab1978789825c9c324" + }, + { + "path": "examples/release1/oa/behavior/process_oa_response.arcadia", + "bytes": 3225, + "sha256": "cf86f46fa5c5d70592ef43f1bc1cb4d968efd8add384ba88036bef6b88aa4abc" + }, + { + "path": "examples/release1/oa/behavior/scenario_oa_conversation.arcadia", + "bytes": 1795, + "sha256": "b25be524e98a35ebac5e4481b7609c027b32baf9605e8097f807560f94a77ca6" + }, + { + "path": "examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia", + "bytes": 1261, + "sha256": "00a1edf2c9707a0eb62ac070c2d02467874f2c9c1f273cc6ddb53d6d65999a1c" + }, + { + "path": "examples/release1/oa/behavior/state_machine_oa_monitor.arcadia", + "bytes": 2022, + "sha256": "447c9ad1359f4a31ecf975ad07c0b08d8e7e03191fb0858e2fc56f377b223cd5" + }, + { + "path": "examples/release1/oa/capabilities.arcadia", + "bytes": 781, + "sha256": "fc8012e5554350b12e1cb80080f8feab80d933c9b33c0f2a7bb500cb7d609936" + }, + { + "path": "examples/release1/oa/model.arcadia", + "bytes": 661, + "sha256": "f7cf914766ea73ec53b85e32d117ea9a9dba52d3e038246863541ae7069b1a15" + }, + { + "path": "examples/release1/oa/participants.arcadia", + "bytes": 1834, + "sha256": "951764c14179f27c10e315acf2ea37d1fff54c77b8441e23addd239e8c715ce3" + }, + { + "path": "examples/release1/oa/types.arcadia", + "bytes": 788, + "sha256": "9d0396fb82dd5733edd8d1cf52f968ca84906927c704f9cf0944a7fae215cf49" + }, + { + "path": "examples/release1/project.arcadia", + "bytes": 1958, + "sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda" + }, + { + "path": "examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia", + "bytes": 706, + "sha256": "73928ced61c06cbc08728d7956676a50fd4c05aa5b7f1ff4f29ce27a0b0f2088" + }, + { + "path": "examples/release1/sa/behavior/process_sa_field_procedure.arcadia", + "bytes": 1790, + "sha256": "886d462af83110cbfa55dd232a667ea1446761234471357cbb878794e98e8768" + }, + { + "path": "examples/release1/sa/behavior/process_sa_response.arcadia", + "bytes": 3234, + "sha256": "041785fc22842423822431bd5e6e8d16094ebd0716965e7c765fb8c1d7549e8a" + }, + { + "path": "examples/release1/sa/behavior/scenario_sa_conversation.arcadia", + "bytes": 2127, + "sha256": "97276f5757ee9aa12a0dbd9d3ccd58174b3091162bb8bcce357cec20a45f5d85" + }, + { + "path": "examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia", + "bytes": 1256, + "sha256": "e12fc406b99d60a2f7768aaa0b25ba560e8605f0b8e37f3a7cdd3b8443562b74" + }, + { + "path": "examples/release1/sa/behavior/state_machine_sa_monitor.arcadia", + "bytes": 2022, + "sha256": "05ce29af99424c491c47fe2450dc6f5f8ce995511c01ec7e17bb5855be3c9736" + }, + { + "path": "examples/release1/sa/capabilities.arcadia", + "bytes": 781, + "sha256": "b6912fb3ece439912345fa71724acd462e716a1e5a88203a1058a5c3b2c20a4f" + }, + { + "path": "examples/release1/sa/functions/function_sa_audit_thresholds.arcadia", + "bytes": 297, + "sha256": "e5a04325d52183956dca814dd90f4e56c96c6b7db0f4f202f203ecb49c3e0f4f" + }, + { + "path": "examples/release1/sa/functions/function_sa_restore_work.arcadia", + "bytes": 2576, + "sha256": "fddeb67308051325041fee641fe758ddf43345b7c47b66d433616ad07fdccdd6" + }, + { + "path": "examples/release1/sa/model.arcadia", + "bytes": 661, + "sha256": "d431509251acb859fbb98733b6d18add952495e10a78703fbe8f31e40bc083c4" + }, + { + "path": "examples/release1/sa/participants.arcadia", + "bytes": 4012, + "sha256": "780d2eb08b217b67b55febb3d34846f727b8ee9018df6c5741748647e36b2ff8" + }, + { + "path": "examples/release1/sa/types.arcadia", + "bytes": 1227, + "sha256": "101fc0fbd99f1101514aa7b6da4f8e535aae0bb94c9e2b9e7e6c151135b28660" + }, + { + "path": "examples/release1/traceability.arcadia", + "bytes": 29506, + "sha256": "48a96f543456c9b5a5798fc795594e4119147fe45f11a6f109f141b7f514560f" + }, + { + "path": "examples/release1/transition-before.json", + "bytes": 35801, + "sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d" + }, + { + "path": "examples/release1/transition-request.json", + "bytes": 4276, + "sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a" + }, + { + "path": "examples/release1/views/diagram_oa_allocation_view.arcadia", + "bytes": 2176, + "sha256": "dfc0eb61e0e2aa5bda45524571fbfa3494f44ce19696f3e15e07438074a07bc7" + }, + { + "path": "examples/release1/views/diagram_sa_allocation_view.arcadia", + "bytes": 2173, + "sha256": "b78b43a26a82efb8fbd5374b09c329fa341f57b29f635d397b9ef8aede71d389" + }, + { + "path": "examples/release1/views/diagram_sa_capability_view.arcadia", + "bytes": 1620, + "sha256": "75e8dc9fb30f25d6f8fd3c6b595593ab4f98958a25798f961e336e0fac557649" + }, + { + "path": "examples/release1/views/diagram_sa_service_view.arcadia", + "bytes": 2209, + "sha256": "b08c84f3ea633e7c78dfdad3272f75415f6256cf3614b6d99bfd4f0269076c9b" + }, + { + "path": "examples/release1/views/diagram_sa_state_view.arcadia", + "bytes": 1931, + "sha256": "5f42f4c605e57b817d8400ed21f2b14bfc4e1592aebe941ff113767504c2b869" + }, + { + "path": "examples/routing-workshop.arcadia", + "bytes": 66431, + "sha256": "dc6b75da72884d399132ed9e300fa5a26b73d802bb526c9af986ae9eeb0a646a" + }, + { + "path": "examples/routing-workshop.json", + "bytes": 100281, + "sha256": "8592714f75b271133f21ce017d87896b6cf91ea0e4837f1c2ed453e21be26b14" + }, + { + "path": "examples/source-project/oa.arcadia", + "bytes": 2142, + "sha256": "5f60642f211b2d352a951092faeeb7503d4fb7f6f7fca99e810347231101d31c" + }, + { + "path": "examples/source-project/project.arcadia", + "bytes": 606, + "sha256": "787f6b67b179ffd221f7e5faf1a4edd74cfd116aa1e3b5e9ed9f22fbff612d8d" + }, + { + "path": "examples/source-project/sa/assurance.arcadia", + "bytes": 682, + "sha256": "ab76044836e4dcd42a82d316846ec87da2108939aa2037640c5bca1d2ecdee0f" + }, + { + "path": "examples/source-project/sa/participants.arcadia", + "bytes": 1231, + "sha256": "1abad579812e77877b45a3d8e3c3e9da734be2d04a0254cc730a5257ebd2dc88" + }, + { + "path": "examples/source-project/sa/reporting.arcadia", + "bytes": 2839, + "sha256": "f0b7443f728912ca163de39b6055876b533a77feaa3219b2d40a2dd65712e058" + }, + { + "path": "examples/source-project/views.arcadia", + "bytes": 3508, + "sha256": "e29ad3f406a6a50d2f0ae2ca2a53ee3b0c18222cd4098704a46d4f48b64ea17f" + }, + { + "path": "examples/text-first/README.md", + "bytes": 1367, + "sha256": "4ec8250d5ed556f1d496e8c85137e973e802ecb11df1a36488411e8cdc5a9f9e" + }, + { + "path": "examples/text-first/project.arcadia", + "bytes": 480, + "sha256": "f441535c623e0cf20e1875eb2cd33b587ed00e078c37af39cb36d89bb5edc19a" + }, + { + "path": "harness/fixtures/basic-editing/README.md", + "bytes": 12012, + "sha256": "3545d9695e7c62e0caa818efd7f4dc426f04876a3626e0f3b1bad606b258cb26" + }, + { + "path": "harness/fixtures/basic-editing/base/expected-baseline.json", + "bytes": 4524, + "sha256": "f239e728d650b0af32bb8f3dcaf6b27c99b9c5041132ac14e51756ea6563d978" + }, + { + "path": "harness/fixtures/basic-editing/base/project.arcadia", + "bytes": 3077, + "sha256": "045cb047d7bfbca72c3fb30db4c81a30ad463d3af7fc51604906ad7fa75a1ff8" + }, + { + "path": "harness/fixtures/basic-editing/base/rationale.arcadia", + "bytes": 57, + "sha256": "c01867c9dc0d40726853119fac3c6ad24a40d8d362a93a8b9d20df04ec2e273f" + }, + { + "path": "harness/fixtures/basic-editing/manifest.json", + "bytes": 447, + "sha256": "d23c21b02064ddb85e051e10e16ff089daba8d9910bb1a695dfabe2db3757734" + }, + { + "path": "harness/fixtures/behavior-creation/README.md", + "bytes": 5954, + "sha256": "9e5fc751424d12cfe76a84abb1da45b2d38947409179451bb90ffe7e7ddc8723" + }, + { + "path": "harness/fixtures/interface-operation-probe/README.md", + "bytes": 2604, + "sha256": "088f0bdb3375deb8f2c7e3b749ca8d210ea828ca4dece69b3871b674e3110175" + }, + { + "path": "harness/fixtures/interface-operation-probe/hashes.json", + "bytes": 977, + "sha256": "f6d3e8aa42f40f64ce2ecba7b9cf112eb9091f3cccde3a592b0434e834cf2a6a" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia", + "bytes": 4218, + "sha256": "c0fe9570684c0061efc4cad65e339fede6961b04ca90c6f324c787bd65e61226" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/after-reset-deletion.json", + "bytes": 7835, + "sha256": "93187f7a1b476a44185725c6b15f2e1767482b6911ab1280144348d033eb51f0" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/boundary-only.arcadia", + "bytes": 4771, + "sha256": "02f85e39957ebe6d56336fa729a5c8b10f51865face9d3b2c9679200571fcfc7" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/boundary-only.json", + "bytes": 8840, + "sha256": "756f9710336e6734ce3592ca774c6f104bba5a37c9e165540eb39326682393d1" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia", + "bytes": 8635, + "sha256": "ac5b629ba5596464f9f8a975b8bc2764e3007ceb7342de3f828d4b18e4f069b7" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json", + "bytes": 16160, + "sha256": "78a47c978dc667aa1c45af711f06aa2a0d3c581003013e30c3e02fb0faae2e64" + }, + { + "path": "harness/fixtures/interface-operation-probe/probe.rs", + "bytes": 11274, + "sha256": "72b1b443e25eb20420dec4477b0fb5ef5d1c705e3882c81df88dd578ab96a426" + }, + { + "path": "harness/fixtures/release1-native/README.md", + "bytes": 2564, + "sha256": "5f5ba040fd2df9261a29bcab3f9a7ae27c3683265411ad3dda0b467d5df8590e" + }, + { + "path": "harness/fixtures/release1-native/artifacts/README.md", + "bytes": 4155, + "sha256": "24200c84ccc2ef3e17c029744074e5cea785188f5549ac54e44cf89682643f81" + }, + { + "path": "harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs", + "bytes": 9275, + "sha256": "444f360b78df52c5e42046f77b148781e0c9bddc2a09c0b7bd65c7a4f376cfe6" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia", + "bytes": 7768, + "sha256": "84b22e95c213018612e6e8a84cad68b35a8b495ff3515b0f978c5e7a3d196d79" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json", + "bytes": 7330, + "sha256": "13a6f99ec2df1dd35ee8b2613f183918cd9537eda4a5da085ad52e65c033db59" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia", + "bytes": 1446, + "sha256": "1e0d662ba33071b122ad9206d2d559a8e748c9431eb3ac1b759bde3760ca877c" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json", + "bytes": 2216, + "sha256": "fa6a2e3643c0f909b92407abec3edc897033162eeaa501f8d4616f79d41fa044" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json", + "bytes": 567, + "sha256": "65cb70b0d697a055363ba01bf30bed7e412d43c9978a033549c378d117719ae7" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json", + "bytes": 3991, + "sha256": "ed905267263e7f8a0cd2dbe833196b30118b6a246815216932fae661ad650699" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia", + "bytes": 6837, + "sha256": "e50fc462ec22dbf98011929931dd2489430001c3e6433608685b9d88bceb9acb" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json", + "bytes": 6433, + "sha256": "f27a7b38d59c7cefe58cb36783d3d4dd8d8ae1168d9aed1eb61d343c18758908" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia", + "bytes": 1807, + "sha256": "eba2aec39e6efb405eace4bc8ab0c5563907c26d449e71a4aa750df9f73233b4" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json", + "bytes": 2673, + "sha256": "2ef777f19b94f08b5e5ca2ef727777897e1018f1426eee291408b3c69101f596" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json", + "bytes": 519, + "sha256": "3af7232ed3056ca84dcaf0f266ead3faaa512a0dc0051c7ba68791c20dbae0cf" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json", + "bytes": 1246, + "sha256": "8af21b00b62e620256fbf50e47c6c5836cdb6922d9549cc4b3d86bc03b008073" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia", + "bytes": 4200, + "sha256": "20bb30473830e44228958e78fb7480269cdcc0580c6c90c718da75f34ca5a43f" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json", + "bytes": 4552, + "sha256": "44f4b05db49758dcf344788f57cecc0250fd029099b1c54e4e1e3e972f6cf039" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia", + "bytes": 1808, + "sha256": "b0fd031671c9f755756922ec5c40a0ce3f40164d22c3c389aec9fd0705344da7" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json", + "bytes": 2673, + "sha256": "1acf81e08c51c85aceb14a3efbc39ec680552a19c7824aab4611ac3cf71b9c6d" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json", + "bytes": 610, + "sha256": "a07978f4b0a3ca83e5ae5f35e7b66b26473b24431fc90e5889d755f1816d52dd" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json", + "bytes": 2352, + "sha256": "5ff844a038b0030adb5a83d13d81e60f30627543388f6199b6a0e026fc42ea58" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md", + "bytes": 4075, + "sha256": "a51ffbc45b6c172cbfed3b16c249296e6a28d457fea57393dd958cdcfbafbca1" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json", + "bytes": 2367, + "sha256": "3ad8b7428b3ed7585a91e7f5d9d5f4880caccfd0b08dca25d05af523e62e0a10" + }, + { + "path": "harness/fixtures/release1-native/communication-means/README.md", + "bytes": 15091, + "sha256": "015c16010c212af283f9def118c6d263ba65a3bc6e81a750f180624ca69bb311" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia", + "bytes": 6098, + "sha256": "a6976e7c21031e7cc3cb731e20c3075cf89cf85144dc8d3f52930f8521ea196f" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json", + "bytes": 10480, + "sha256": "60c4419415c546f06c9972ac7bb0e530ada7b30a200b59e75ddda231a24f9a3d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia", + "bytes": 407, + "sha256": "3c41459f2b0706a767988738bf8126f76540415bb5940d9c5db19cbe8c18182d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.request.json", + "bytes": 1331, + "sha256": "b16e673b29c96b5aed681389ff7669110abe552939e86dd0ec6011c9a45ed860" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-draw.arcadia", + "bytes": 5239, + "sha256": "a511a4d817d9c5c80233bce83e84a7056b7dce7afd9fcc25696e333f8f4df055" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-draw.json", + "bytes": 8424, + "sha256": "7140114926228b1c378c450535f7a70992c71a879a8d2270c49e8d5f04a2f484" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia", + "bytes": 5710, + "sha256": "910e6dfe609467047ce629ec9c626f4e927cada74bfbfa164414a8fe6783ea87" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json", + "bytes": 9868, + "sha256": "567259e20c9f6278776e8fa99b8b2fdbbc9a883ad875c5d39dae1b53de3a2924" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia", + "bytes": 407, + "sha256": "997f72fe9258012df66e88344cf39c1a2cebf2327f604056b67f7f2a8ebcfbfa" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.request.json", + "bytes": 1127, + "sha256": "cb3fe05859cfffec6f8846c19983dc462413bc0ea4f6e19204f09c259919c4b1" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia", + "bytes": 8209, + "sha256": "ec826b9b84616507490ac30e4d57100dd6338dba2f7071e879a8fb58820a85c5" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json", + "bytes": 19735, + "sha256": "ef07e4d2d3fc36004ff01739838281581d9a703c747611eacc01bb9cc672489f" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia", + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json", + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia", + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia", + "bytes": 9827, + "sha256": "86dd8a355f6c9dadfea56d06e5247c9b0f820819d7df17ef5690671c7c81324b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json", + "bytes": 21877, + "sha256": "980489ec2c483d4f9efada61358c1f29bafb2572fd5b804c656b91b8e9bb4f9a" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia", + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia", + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia", + "bytes": 7698, + "sha256": "1abc4c4cac37cfb06ed3b12a115f9e7e6a2d3affd34b25c355309e5ee465fa40" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json", + "bytes": 13130, + "sha256": "a5cf4cb2ef390113dd752f4054d43fb789da2ca77799efb5782805ae8aa2dfd5" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json", + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia", + "bytes": 5454, + "sha256": "b761c4c2af5f7064aaf1f887ffd6094eb6ae834b2c8057ecdbc4aa97c042066b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-transition-ready.json", + "bytes": 8852, + "sha256": "2536b000de361fb131787e32b466b9123d4e98abf00c099ad32f6cb8e7fb530b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/generate-communication-means.rs", + "bytes": 28035, + "sha256": "003521404b702332082e01f2be7676fda1dd7b5bcf220bb81d25120c8984c75d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/hashes.json", + "bytes": 3133, + "sha256": "39b4dc02fc8b074015c59354fd1c1061c25832fde25065c7151f36eb0455e6f6" + }, + { + "path": "harness/fixtures/release1-native/communication-means/manifest.json", + "bytes": 1237, + "sha256": "fb921f42f2c4a4f8a67d1266f17f8c7750b6a2619554b713aa92b17918f0c43e" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40-oracle.json", + "bytes": 26264, + "sha256": "74edca148583f42c269e51f135662f65ef1c9ea1c94516d63e2a5091967dc99b" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40.arcadia", + "bytes": 28947, + "sha256": "00b87736b82c5afd33b311df7aebe9a6c8c51b6de66221181b5ea8bc8ca221a3" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40.json", + "bytes": 54938, + "sha256": "5dd4f22453330cab3abc059881660190e689a22619853f77cf6d0b52735d97ad" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments-native.md", + "bytes": 2624, + "sha256": "28906924b05f75bbf432389517aef65f1df474d2f2bff79a3586564f7f7328ef" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.after.arcadia", + "bytes": 6795, + "sha256": "9d084dd75a0251d349cc31f9ee98df58d0bc76859dc3cb1503f242e2f759888a" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.after.json", + "bytes": 9618, + "sha256": "f09be41afd85c18fd52fdf997e9b81c4b7cb2c41e78d167abcd14caea8110333" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.arcadia", + "bytes": 4990, + "sha256": "8740f27be99624013d9be876639c0411e1b8c14f0f9a843f43dc71d769efd79a" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.hashes.json", + "bytes": 963, + "sha256": "00fc9e2c709bdcd9ee98012176cb0edb635c112153d963cd169bf4bf69a9e010" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.json", + "bytes": 8146, + "sha256": "9c188f2890630c7feec2be719a7104af9690da7ea836df18fa78da3ff2acfc1b" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.manifest.json", + "bytes": 1045, + "sha256": "46b40f221ffad614733b615208814ace63947e96b80bbfdec63853ccd4f4e8c8" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.request.json", + "bytes": 3075, + "sha256": "b57f656f0b9cf2c595e83150c683301909e20f15684d35d108d3c57ee0dc5909" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch-native.md", + "bytes": 2637, + "sha256": "dddca2b85f892b05881c75ef3cd62437396cc086a09909b621e02bad0cbe56ff" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.after.arcadia", + "bytes": 3134, + "sha256": "d3315bba762395205ae761e6e64029e8b4070c15a1299c6114075581db7b0974" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.after.json", + "bytes": 3927, + "sha256": "8f36119c7e893a4af374b13b85b1c2cb3df0196df8d06a551386628fc8b8646d" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.arcadia", + "bytes": 1787, + "sha256": "5e1f433e04621424edac43e53411624059e873f88dd531bfdb39093953485ed7" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.hashes.json", + "bytes": 933, + "sha256": "1bd87700b04e4f169ab5f8aceaa3b0c3ec3a1cc43e1bce135a1cee506fc75d38" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.json", + "bytes": 2816, + "sha256": "1972242d2a8677d004dc3af1081feb292de65fe26128d877dded8661f9906619" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.manifest.json", + "bytes": 647, + "sha256": "1e440efa02e2ccc27585d9a49c7864d53e3e3e7645ccb219f040e00d95331b8e" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.request.json", + "bytes": 2197, + "sha256": "71f23474614aab90398ce24f00f2cf61d6cb9a3a34faa3e12e0c8104e0a71073" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-repair.arcadia", + "bytes": 15635, + "sha256": "e3ce0347746584f503b825a8b6bd23a7eebf0827940112ef8f36d500c1d0a043" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-repair.json", + "bytes": 25736, + "sha256": "29736f2a619f7f2d0e795229f6e8c3645a1b4eca6292043cd604aa01b1ec9170" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-take.arcadia", + "bytes": 15631, + "sha256": "9ab573c92be66e3bae659dd826923ba6066979b1651edda7bd469056632cd77e" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-take.json", + "bytes": 25736, + "sha256": "828c990c6129692cd31c7f6ed0dd6c952f9cfffdd90bc80171df82d4d929afba" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.arcadia", + "bytes": 15617, + "sha256": "49214be2ca9c8bb6195c3b39737c053847021839f70f0bb2921d35a1fbd2ac83" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.invalid-request.json", + "bytes": 2225, + "sha256": "8b02d4739b1be2c8a64da76423b4a6cae7d89c60f4d30723b94f1da3779cbcca" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.invalid.txt", + "bytes": 78, + "sha256": "aa536e18bc65325163e9b645a4ad031ab599fcb05948dcf741b725d0f0c8b602" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.json", + "bytes": 25722, + "sha256": "995dd72f5c1f07f998233ae9847e337cb82de0c2a9053d3905be021d5f8124b9" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.manifest.json", + "bytes": 1059, + "sha256": "ad315359bfd68ce0d265976dc9e4ea6a502ebbb87d7339dd1af5ef27dd01e433" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.repair-plan.json", + "bytes": 15016, + "sha256": "27bf316ad66ffa91f6323e75b0c280baf5caec298645793dea72c5d42aa3b782" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.repair-request.json", + "bytes": 2225, + "sha256": "f4f6b5480ec8927033ea469ea82c1e8411cb6dba95d2da680f15bc193914c9b4" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.steps.md", + "bytes": 3897, + "sha256": "7d7e38d93641a5f22a2738baa4dbacc392eebb477a855cdce0641961980a19ad" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.take-plan.json", + "bytes": 18557, + "sha256": "74dd68ba686b8928054480f0bfed55aad91f6f1208803399dc9878e894f57ecb" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.take-request.json", + "bytes": 1508, + "sha256": "d19a53002a6bf95af3123a3ef9f78baaf7ce1085d4f06f0f4a9d5e5d5accbc0e" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.arcadia", + "bytes": 6935, + "sha256": "90277f1780d43a6f0b2184bf00b87e7a4f3518d67c6f10d4793daafe1aabf493" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.json", + "bytes": 11286, + "sha256": "5a3c8494f6976c23b990c14b40760fb2f32ca9973caf6f31dab5632310663591" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous-request.json", + "bytes": 1379, + "sha256": "6a86fde194faa55f2d1bdb982571e0e711e5401b53a960e6acb8cf3e17f506c8" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.arcadia", + "bytes": 6195, + "sha256": "e7c30c5a5c3ba7209c8e223f02da55821d7af10a2f09844a7065ae6cd8f22aa5" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-plan.json", + "bytes": 19156, + "sha256": "d1e5c2ed08e0aa6274a0421a39876d1ba99f4e441c7a0a68660a83b9e017da14" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-request.json", + "bytes": 2418, + "sha256": "433f723252598994dac7184da61f95344b528156cc5a0fba1a7864b2ec9f002b" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.json", + "bytes": 10441, + "sha256": "9e4e2ce1c7879935ad0860a3ae720506c929a8422b51491b719d88af3fd3ff2d" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.manifest.json", + "bytes": 1042, + "sha256": "87e8899c7d4e54faedeef32afb7592fb65278d1bd7390850ab198dcf62068329" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.arcadia", + "bytes": 6280, + "sha256": "6c77ed42bbf9f4ebe08bfa233197a4020e5587ce33747f7914430aa0c662d9a3" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.json", + "bytes": 10009, + "sha256": "1dd7ca2bcfc84ac23369c506302b39569d2ebc41e5a4edc99378a1fe01923b7e" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous-request.json", + "bytes": 1390, + "sha256": "e0ea7aeb25687ae63c65128f1aab78273f14030d179d3fbea63fe91ab5430136" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.arcadia", + "bytes": 5529, + "sha256": "563c36162050c8564ffa25a85c726462ecdaebdb87659b3f3610506bfd5581e6" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.final-plan.json", + "bytes": 18821, + "sha256": "f96c43fc58dbe48a044b628b31bb3f0b59171dad5b6982ec6c34a5597e474130" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.final-request.json", + "bytes": 2438, + "sha256": "d38fdb13612b07254edad80848f79c1e097d5f868c020feae9ba072aa697a50d" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.json", + "bytes": 9153, + "sha256": "2ed0daed118f2df13dcad738dab174ad34d44a15921cf8097415e707978b0968" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.manifest.json", + "bytes": 1058, + "sha256": "c79bd12a85d8d4f25cc3ee43581b3e5e74ae9ad7e4de0c72764975ba2b89c279" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.arcadia", + "bytes": 5026, + "sha256": "98020e048ac01f285c6ad8c10ce22bef05e193b5ba0f5558be1fb26a7bf82620" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.json", + "bytes": 7851, + "sha256": "ea3783992488bc6ee944eb91a26ccf3dbc05a3909528fc32ec8023a76765cee6" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous-request.json", + "bytes": 1379, + "sha256": "ea3cfd25f7a50481805956fb88ab851516dcd602dd6564502a0cc55f7de6a54f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.arcadia", + "bytes": 4295, + "sha256": "2b7c3694623bbea3e36dcc6f674d4adcd91e23c8e1005ded21e5a69648a048e5" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.final-plan.json", + "bytes": 17912, + "sha256": "95a23bd334cffcaef0c3b1012b7555efd2a077ef6f9140d58020910ef8a76723" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.final-request.json", + "bytes": 2081, + "sha256": "1e2433ed5821bf65443f56cb27381ee935db81b1955d3a539b7952898312a410" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.json", + "bytes": 7012, + "sha256": "0033f8780c2ba69779b811bca792340c25a092f887e5811c6839f0a691d744f2" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.manifest.json", + "bytes": 1046, + "sha256": "e3b4a60a56abee779ca8356605c968aa2d99256a3bd3c5601b14b5c5502a1a05" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.arcadia", + "bytes": 5467, + "sha256": "e265cab35097acc46f5123107c13bc35b9f71931ff650557fc1c85779141b327" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.json", + "bytes": 8482, + "sha256": "9b777828d3681138de6bbd08de8bf922bed92c27be60b103d8ff73ce7ddd0a48" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous-request.json", + "bytes": 1379, + "sha256": "7d124256730bf2feee772c7d0118b87eb288e8aaa9179d69ceb9259dbceb2c71" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.arcadia", + "bytes": 4727, + "sha256": "87a5a3665dab2dd6e595867dcebb1586e93dd067c85d6e24b1a4d3ff0ad6d4ba" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.final-plan.json", + "bytes": 18803, + "sha256": "1888ca51e0923866a9afea99cad069fe5d3f19f0a1b24328e20ac1b9f52d8bad" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.final-request.json", + "bytes": 2294, + "sha256": "5070a5b85feb1bc7b1fc648cd5d35ef26663492c8666dcd471950c06ec3a6052" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.json", + "bytes": 7641, + "sha256": "725b35e36640c81c70c7efea5c3304bf61b49ef9664194ca0d491782cc62191a" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.manifest.json", + "bytes": 1038, + "sha256": "1b43abf8fed3061c65a7b7cb103d31ea7ece333a2da83c645f8d228448c4dbb2" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.after-order.arcadia", + "bytes": 5019, + "sha256": "65a6217f474b5085f37b511151185c2c08ff7a1998e924f1fe01f29c410f6ced" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.after-order.json", + "bytes": 7835, + "sha256": "8a53bb9b43eab5b6dca5e08deef20c14c96e134b9ee816a99b7960bb5255e008" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.ambiguous-request.json", + "bytes": 1379, + "sha256": "c704f99245f00d5c3be0562d91931b4e7f1fb3fb36f41991eb76656ffbd6129f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.arcadia", + "bytes": 4279, + "sha256": "c59ec1233d5a9fbb8e386f6d9b13f398be21baf813ba215efb51bfc1bb98db36" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.final-plan.json", + "bytes": 17938, + "sha256": "30c5c3f614a74708763df612016483623792f4c65e2ed2fa662ac417cfb852eb" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.final-request.json", + "bytes": 2098, + "sha256": "3b6c35ef78133f0980cb6fb53fbc29c5d3c4deb5dc4be01c2c3384e221987b9b" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.json", + "bytes": 6996, + "sha256": "c159c60403abcf8fefab7f19cbad9989488e8ccc20b82c57597b22a0ae254802" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.manifest.json", + "bytes": 1038, + "sha256": "af8a314a9b1d85a8f98affa5a6649f43d252851158798e1ad687bb1b6966b14f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.steps.md", + "bytes": 4549, + "sha256": "6004894e9d9f478b7fe1c3c3e0706dcc546d5df8557cff91b4cf26f8a4badf24" + }, + { + "path": "harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json", + "bytes": 1933, + "sha256": "71cca2a59f36b2a07d11dedbe9101226aa1a4a00d8ac244b5565202e56709f51" + }, + { + "path": "harness/fixtures/release1-native/source5-recovery/generate.py", + "bytes": 1496, + "sha256": "e44986c1d563499f25917441220b23883fc9cb0f89faf409873191f0212fbd2f" + }, + { + "path": "harness/fixtures/release1-native/state-actions.arcadia", + "bytes": 6224, + "sha256": "9cde82608110e56aa9e14c9e33eb38976f53aed74ab013145d411d5743a0ec26" + }, + { + "path": "harness/fixtures/rest-coverage/README.md", + "bytes": 1870, + "sha256": "df4a391312e0c8b20ccdcc850199443b08547be1b10b1f2447a067bf4bf544e6" + }, + { + "path": "harness/fixtures/rest-coverage/coverage-probe.json", + "bytes": 7951, + "sha256": "2d1ef7bdb5d7bf17b9fda33b1d1790d1d88289b3fd1e725f89b83a69771f9a43" + }, + { + "path": "harness/fixtures/rest-coverage/run-coverage-probe.ps1", + "bytes": 9381, + "sha256": "f24f19fca418b76a7249894a08c116e16b465a02f8f899d12b808ccfdd90a3bd" + }, + { + "path": "harness/fixtures/run-timed-bundle-oracle.ps1", + "bytes": 7466, + "sha256": "fab696486a574f9867c6e69a8a8b960e7e9722206441b9cc5f36d41f285f1a25" + }, + { + "path": "harness/fixtures/scenario-authoring/README.md", + "bytes": 14700, + "sha256": "6b651b180b76d10b00bc49d77a8f5e0b3ba0317446978f41b541f5bb23fedad8" + }, + { + "path": "harness/fixtures/scenario-authoring/base/expected-baseline.json", + "bytes": 38589, + "sha256": "abd8246c939ebb8a2162ef82e23d0fb322ee1a8b4d0d7c28a34037e42e86e841" + }, + { + "path": "harness/fixtures/scenario-authoring/base/project.arcadia", + "bytes": 19470, + "sha256": "9b1afbe96b0bf8fa613451b00929492f05a59b9eb2ca74c2c374a8b7bb7cba9e" + }, + { + "path": "harness/fixtures/scenario-authoring/base/rationale.arcadia", + "bytes": 86, + "sha256": "82df210f22f6aa336a3e7e00240977d671c7c3923db7bc58fb29e6d98b67ef49" + }, + { + "path": "harness/fixtures/source7-native/README.md", + "bytes": 7235, + "sha256": "7abccbc3da0980d1d099761d1a66d12c403a0f543954134537ecd4930d182b4b" + }, + { + "path": "harness/fixtures/source7-native/base/expected-baseline.json", + "bytes": 26428, + "sha256": "e2882a58f841f460f45d9d67db1606440e0d7997b9b29a784569ae48e4b23bfb" + }, + { + "path": "harness/fixtures/source7-native/base/project.arcadia", + "bytes": 13616, + "sha256": "5474d8ca05fd1375220dc0ad40d913631fde51159e9632bb9f290d1ecdab8dd7" + }, + { + "path": "harness/fixtures/source7-native/base/rationale.arcadia", + "bytes": 59, + "sha256": "e6a600e13036d4ad594a65380ed1b3d82d1772d583041bb0a624c9588a73047a" + }, + { + "path": "harness/fixtures/source7-native/manifest.json", + "bytes": 410, + "sha256": "6c57164a796234d83818d462d0a25f10e77062d405769e602ae2a7bec251e3b6" + }, + { + "path": "harness/fixtures/staged-source-authoring/README.md", + "bytes": 6876, + "sha256": "502be5e3bf9112447dc07b1a51fe0fa301754b47128a9b81877c185c6e797fee" + }, + { + "path": "harness/fixtures/staged-source-authoring/base/notes.arcadia", + "bytes": 63, + "sha256": "ad9de02963c20d1f43856455a797deb05ca674659e45f2c4b96b52783024f8bd" + }, + { + "path": "harness/fixtures/staged-source-authoring/base/project.arcadia", + "bytes": 593, + "sha256": "aa844950ec4fb86f262a257e1fc370a26557ea7978d8a4cab78265037660234c" + }, + { + "path": "rust-toolchain.toml", + "bytes": 66, + "sha256": "a6a0bbd29ffaa8182dc22d1d9149709f1091e47df40ed96eb8a78a711c66a4ce" + } + ] +} diff --git a/harness/experiments/canvas-union-windows-20260911/guide-82fd0ac.log b/harness/experiments/canvas-union-windows-20260911/guide-82fd0ac.log new file mode 100644 index 00000000..0d185c07 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/guide-82fd0ac.log @@ -0,0 +1,11 @@ + Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\crates\oa-desktop) + Finished `test` profile [unoptimized] target(s) in 31.49s + Running unittests src\main.rs (target\debug\deps\arcadia_studio-e358a5ed59b1de41.exe) + +running 3 tests +test guide::tests::filtering_and_related_navigation_keep_selection_visible ... ok +test guide::tests::guide_topic_navigation_resets_only_article_and_same_topic_search_keeps_scroll ... ok +test guide::tests::search_finds_concepts_in_walkthroughs_and_api_reference ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 726 filtered out; finished in 0.10s + diff --git a/harness/experiments/canvas-union-windows-20260911/guide-82fd0ac.result.json b/harness/experiments/canvas-union-windows-20260911/guide-82fd0ac.result.json new file mode 100644 index 00000000..6e00f889 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/guide-82fd0ac.result.json @@ -0,0 +1,7 @@ +{ + "source": "82fd0acf64d87f322e12602154f14a565b29de4c", + "start": "2026-09-11T18:03:59.0931875Z", + "end": "2026-09-11T18:04:31.1587860Z", + "exit": 0, + "command": "cargo test --locked -p oa-desktop --bin arcadia-studio guide::tests -- --test-threads=1" +} diff --git a/harness/experiments/canvas-union-windows-20260911/guide-inputs-82fd0ac.json b/harness/experiments/canvas-union-windows-20260911/guide-inputs-82fd0ac.json new file mode 100644 index 00000000..a13e8ad8 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/guide-inputs-82fd0ac.json @@ -0,0 +1,2595 @@ +{ + "source": "82fd0acf64d87f322e12602154f14a565b29de4c", + "inputs": [ + { + "path": "Cargo.lock", + "bytes": 119973, + "sha256": "065c23c94908fe499d32ed91d55a70946ed5b08ecfda3b483ec1e635572257bf" + }, + { + "path": "Cargo.toml", + "bytes": 400, + "sha256": "369f5b4b2b60705a94623c8c0b68f8e0b048f263280ce07ae0629ad353797d62" + }, + { + "path": "build/windows.manifest", + "bytes": 354, + "sha256": "9c19c5a02f858ece84c44839ad2b145d9c1d90442da0b15c87fd045f975974eb" + }, + { + "path": "build/windows.rc", + "bytes": 24, + "sha256": "a27385edcae0bcf96f55361056cb7be5f36d76fcddcaefd20647c91959563e1e" + }, + { + "path": "build/windows_manifest.rs", + "bytes": 1460, + "sha256": "168da2c42ecccfebc11ee3e1b455e653a68fe9c6d30a9842a8b9e7441d086c80" + }, + { + "path": "crates/oa-cli/Cargo.toml", + "bytes": 279, + "sha256": "49c567f1af74a826683b4d96845feb3806fd9bcb7dc2767e16b5509d4e5e1cde" + }, + { + "path": "crates/oa-cli/src/lib.rs", + "bytes": 15498, + "sha256": "97bebab63b4a611a1d36ad0c20fe452cbe1985fe3a6fbd39c954603da11410fc" + }, + { + "path": "crates/oa-cli/src/main.rs", + "bytes": 2984, + "sha256": "de964f9b6c59c0f31c080afbd3b31d442b07b1b44f6a187a248d3409c45d35fb" + }, + { + "path": "crates/oa-cli/tests/source_cli.rs", + "bytes": 16600, + "sha256": "a3a1b856c7a9fd3d80bc8074150a24fbb0ef0e9ddf736c7cea036725fc3dc2b8" + }, + { + "path": "crates/oa-core/Cargo.toml", + "bytes": 408, + "sha256": "6b5c062474ebb62d5082e07dabed99ee85e4a833edf26690fb4f887028d3416f" + }, + { + "path": "crates/oa-core/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-core/examples/generate_demo.rs", + "bytes": 795, + "sha256": "e26f432c95331aeca50985dbb3ad2c558affd43023b585811f0f7bfaa59f6661" + }, + { + "path": "crates/oa-core/examples/generate_release1.rs", + "bytes": 1798, + "sha256": "1d5f11165d74893e4bc18dbf3a2f2111f4026bd89fd06ee7f0bae69e45c4d4b5" + }, + { + "path": "crates/oa-core/examples/release_dense_views.rs", + "bytes": 14664, + "sha256": "a83158488ef9b7394f136c52b1496719be935c4e5bdfe457cab5c9894f5f9412" + }, + { + "path": "crates/oa-core/examples/routing_workshop.rs", + "bytes": 17248, + "sha256": "571307b18b6d4fa04700c5a4d4b0c2154c419e93a21292b2028e7b6f46a2faa7" + }, + { + "path": "crates/oa-core/src/analysis_numeric.rs", + "bytes": 3735, + "sha256": "6c88eef3119162aeb6c07b9e75812c3af9fbfe75554b0503ad00f103cb478408" + }, + { + "path": "crates/oa-core/src/analysis_numeric/dimension.rs", + "bytes": 3045, + "sha256": "d4ad8e5307a1620e31791d724a0e31fdba860d00dc83e3e1f2209d9c5e22a340" + }, + { + "path": "crates/oa-core/src/analysis_numeric/exact.rs", + "bytes": 17338, + "sha256": "4eb8cb0f66b10e5da204899af82ee26adfec74bab47b1a6c9842b5488ee04c4d" + }, + { + "path": "crates/oa-core/src/analysis_numeric/fixtures/r7-literal-boundaries.json", + "bytes": 6085, + "sha256": "3b959dec5e100a6f03817f5314bef497347ba579ebd235476516d4bd83399181" + }, + { + "path": "crates/oa-core/src/analysis_numeric/meter.rs", + "bytes": 7129, + "sha256": "1f7174beb992935b4c399acdef91114af73faf041070feab968455ac827e8ee4" + }, + { + "path": "crates/oa-core/src/analysis_numeric/quantity.rs", + "bytes": 12257, + "sha256": "ead5142d6eeea8f99399d57e02d8f7d819980923de76387cd454694d6a747aee" + }, + { + "path": "crates/oa-core/src/analysis_numeric/tests.rs", + "bytes": 27486, + "sha256": "323ce82fc057d36f44463569fd8ea324c7d700fddfbf32bce15e04bd2a66970b" + }, + { + "path": "crates/oa-core/src/data.rs", + "bytes": 42696, + "sha256": "7f9c7c7dd8353cb5a38597b92cde6d3e890203d392473187eb387d41ff37414a" + }, + { + "path": "crates/oa-core/src/demo.rs", + "bytes": 20657, + "sha256": "ad62123f96c20ff023cafc1a9849b0fa06bc62edd1fb9701799e4599b458600c" + }, + { + "path": "crates/oa-core/src/language.rs", + "bytes": 99938, + "sha256": "638f08a42e9cf4996b5d07f69b4d6f517a518369f3604d25be1006e40e080765" + }, + { + "path": "crates/oa-core/src/language/semantic_source.rs", + "bytes": 7326, + "sha256": "584b5bb59d87a7b8d44d5dd2d124c3a1323b21492b138a17bc9526c0087c9e29" + }, + { + "path": "crates/oa-core/src/language_nesting_tests.rs", + "bytes": 20639, + "sha256": "bd061611e8cb6f28ab203c73e544ac98bf275eb464cd314ad5fbd25f773df33b" + }, + { + "path": "crates/oa-core/src/lib.rs", + "bytes": 549, + "sha256": "ea1d9bf5acba8381ba204db12ff5b5fe1f320417d17af517e46b90fd6bcb241c" + }, + { + "path": "crates/oa-core/src/migration_v2/README.md", + "bytes": 1515, + "sha256": "fb61d3faa33d7bdfba73415415a0882c0fbd3e0b45df42f972a19aec6da927b9" + }, + { + "path": "crates/oa-core/src/migration_v2/adapter.rs", + "bytes": 9490, + "sha256": "66741b4c8354a23b8a932ecc5bf1bc3bb6e74279558757b8c35bc2157a7e51b5" + }, + { + "path": "crates/oa-core/src/migration_v2/mod.rs", + "bytes": 1158, + "sha256": "857d8bb13b6814daba6780ca770c16352943c01cb130245d055f11b3b957b3e9" + }, + { + "path": "crates/oa-core/src/migration_v2/model.rs", + "bytes": 52059, + "sha256": "8ac63eb59b2e57bdecff2477ee0634096090924d3dd1851e1c454880a889dacb" + }, + { + "path": "crates/oa-core/src/migration_v2/references.rs", + "bytes": 13369, + "sha256": "33a6819abc83139fdaebeebd1b553238a328637106cce7dd5a535f1791d43e20" + }, + { + "path": "crates/oa-core/src/migration_v2/source.rs", + "bytes": 21946, + "sha256": "74fee9b24e7c912182c998ab9d9aeec125e2009c3214959229e844c67afe2bf6" + }, + { + "path": "crates/oa-core/src/migration_v2/state_validation.rs", + "bytes": 15769, + "sha256": "e29ef071bab0a68460ddf55c46131c6f66c022d52c3c602c37c013c3649da1b0" + }, + { + "path": "crates/oa-core/src/migration_v2/transitions.rs", + "bytes": 704, + "sha256": "f74174c532ba56b57c80b719a11fb4adea0a78ccc2828fff72fe2b9eda5a10b1" + }, + { + "path": "crates/oa-core/src/migration_v2/validation.rs", + "bytes": 98736, + "sha256": "6d5e4a4e2805ae5045bf789a0485e14a61c63346587bace2b4a91173a688f780" + }, + { + "path": "crates/oa-core/src/migration_v3/README.md", + "bytes": 2128, + "sha256": "3755a63f840d7f0eadf68afa7a9d587877a9468ff982d3610539e8a0bb067a82" + }, + { + "path": "crates/oa-core/src/migration_v3/mod.rs", + "bytes": 23023, + "sha256": "f4edb6a0b50ecfe8670973776d703047b064420cb1f05913129c00fc656b3722" + }, + { + "path": "crates/oa-core/src/migration_v4.rs", + "bytes": 4020, + "sha256": "50c652964f0f309e7f3f54b6ef12c40d42fcecc0490f005995c713df1179af70" + }, + { + "path": "crates/oa-core/src/model.rs", + "bytes": 66352, + "sha256": "4585c895c64c4a183941c92cf047a9e52ff0cbe41971e16e78d229ea9dc101fb" + }, + { + "path": "crates/oa-core/src/operations.rs", + "bytes": 11405, + "sha256": "a5ffbbf54cd440316a106d1188bfb699b57dae59f466edcb33618c333cd19f41" + }, + { + "path": "crates/oa-core/src/persistence.rs", + "bytes": 31774, + "sha256": "1c01474f906a63e552c0d590b272c95821c00a100b3e79e58e67dbbeac341a14" + }, + { + "path": "crates/oa-core/src/ports.rs", + "bytes": 45413, + "sha256": "5cac12eea70a54ebebbd6be77b63ce92e3195325c4bc7d27705c518686cbac80" + }, + { + "path": "crates/oa-core/src/projection.rs", + "bytes": 21374, + "sha256": "69ed12bace2cc5024e151752a174ab14034ac2d05eb741fcf13e9a27521c059b" + }, + { + "path": "crates/oa-core/src/projection/port_layout.rs", + "bytes": 13474, + "sha256": "a08d17d10a58bb8b206dd001b19b91d5aa3a9f743b298a4da57d503223c20079" + }, + { + "path": "crates/oa-core/src/projection/state.rs", + "bytes": 5233, + "sha256": "73a607bac713ec7ff49a509fefb1a53c7686dca93ee241ffc835564d743afb28" + }, + { + "path": "crates/oa-core/src/references.rs", + "bytes": 21957, + "sha256": "93553c234f32da35f1f41a7b83ad81c471c9803efa78ca222cf7de3c2f86c0bb" + }, + { + "path": "crates/oa-core/src/routing.rs", + "bytes": 50309, + "sha256": "c5ebb350ba76f731f5c48d968bfbba4b6ef68ec046eb5893d116c4637ef15080" + }, + { + "path": "crates/oa-core/src/semantic.rs", + "bytes": 540, + "sha256": "f9895dd5b8fb27d10d4cfbb41c3542ef2018365a589b8d5778e5674ae5a1f239" + }, + { + "path": "crates/oa-core/src/semantic/catalog.rs", + "bytes": 49407, + "sha256": "1ff74fcca18084d178681d7ff38ee1ccac632c27c4de0595ba1ceab8067ab3a4" + }, + { + "path": "crates/oa-core/src/semantic/historical.rs", + "bytes": 15271, + "sha256": "eaf3284bf234312698514cdb636bc8348e1b5d746cb16b18df777e77d312abfb" + }, + { + "path": "crates/oa-core/src/semantic/linked.rs", + "bytes": 6035, + "sha256": "6f335b14e809aecfc35defcaf77cb821b080d1506dbc46297912b25af98c07a2" + }, + { + "path": "crates/oa-core/src/semantic/mutate.rs", + "bytes": 22683, + "sha256": "dc2b4c4a6eb156fe468eaa8c85fd37a505c0cd8ea1f226cb69cd01689afd3d9d" + }, + { + "path": "crates/oa-core/src/semantic/review.rs", + "bytes": 51116, + "sha256": "0c157d2c327e72e89cca3b423e71ee73798105c5bb7271439c1a101642c91e3e" + }, + { + "path": "crates/oa-core/src/semantic/types.rs", + "bytes": 10405, + "sha256": "6922fe35089f12fad43ab0ce1c6886d01a3855f90b8e2543e99802ac5e6f1bbb" + }, + { + "path": "crates/oa-core/src/session.rs", + "bytes": 35237, + "sha256": "1eb77b329b2986fc6f5dc0171e77fd860375cdfd8c769a1367b445d6c49ebe00" + }, + { + "path": "crates/oa-core/src/source_files.rs", + "bytes": 74803, + "sha256": "669491dbddb8e8e6556161426dffa1a49ac1c826d31020bff0956d81a4d84683" + }, + { + "path": "crates/oa-core/src/source_project.rs", + "bytes": 61843, + "sha256": "1394370bcc5908873b830c2c11312e6e1611af41f46c4f43ca0a6bcb713bb466" + }, + { + "path": "crates/oa-core/src/source_project/source_migration.rs", + "bytes": 4700, + "sha256": "add463ae85c420f853d324b00a221f0f874164cc3f3ed6bbb07557a9ed7a8c95" + }, + { + "path": "crates/oa-core/src/source_project/source_structure.rs", + "bytes": 20225, + "sha256": "9f1f83315856a7025fe662c0fbb13452f31613224a03663150d6225d1ddc162f" + }, + { + "path": "crates/oa-core/src/state_validation.rs", + "bytes": 15665, + "sha256": "e28d7f74385e2a78763716b34491bd73a00bfeccc085970ceb130627e3faee18" + }, + { + "path": "crates/oa-core/src/topology_context.rs", + "bytes": 13734, + "sha256": "16de69b297de95d93c73808621607833b713af811dd082f0169186ae1e5c3e2d" + }, + { + "path": "crates/oa-core/src/topology_context/analysis.rs", + "bytes": 20036, + "sha256": "dead6194f0aa60cc46045c0e5ee10dbdbb689158c38634dfbbbc45d2ba88ac65" + }, + { + "path": "crates/oa-core/src/topology_context/analysis_tests.rs", + "bytes": 5258, + "sha256": "17839263410815e96fe027d4eba3aae0a9ab3b82b16947e2b29ebe0d09782b56" + }, + { + "path": "crates/oa-core/src/transitions.rs", + "bytes": 51998, + "sha256": "fadd1b07b89cae4117b91abb4f1ac9ea81ff818ef20f2175e76ee8cf21fb708d" + }, + { + "path": "crates/oa-core/src/transitions/communication.rs", + "bytes": 8634, + "sha256": "2b476630a88edd9021b9156ca12a5402e01604095f0d4002afb452671903a028" + }, + { + "path": "crates/oa-core/src/validation.rs", + "bytes": 112792, + "sha256": "7c1b4ae5e4ebba13e99af9158ccde138dfc5e9a7afb580e76fba72d0b64ba74e" + }, + { + "path": "crates/oa-core/src/validation/coverage.rs", + "bytes": 6478, + "sha256": "b08e4b344ecad055714168c83beff7fe29ba4edc53edbffa2eae04e5580a4d5f" + }, + { + "path": "crates/oa-core/src/view_options.rs", + "bytes": 24481, + "sha256": "421ede0a04c593ca1df0e6e0d06fabcfcc8113cdbcfb0adc7f5c8ef4d1724208" + }, + { + "path": "crates/oa-core/src/view_options_tests.rs", + "bytes": 21433, + "sha256": "64aac44de2df42a1012239a3dd75c473daf0c8990b3170b7bf7e78a556080208" + }, + { + "path": "crates/oa-core/tests/communication_boundary_scenarios.rs", + "bytes": 8816, + "sha256": "e1100b8f363910a487d90c73acc7df543dc6d532158c3c63ad11cd3d40d8e12b" + }, + { + "path": "crates/oa-core/tests/communication_means.rs", + "bytes": 18102, + "sha256": "a25f2749180bc6f4beb1445bfad3351eedf0669131a7439b633ab804439e95a4" + }, + { + "path": "crates/oa-core/tests/coverage_diagnostics.rs", + "bytes": 20527, + "sha256": "e2e9c3996daab47f2bd23f5d8c48b4e372af33331152f5fc85b76f06d04fcdf4" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.arcadia", + "bytes": 29220, + "sha256": "f0df129c1914bfc01d0b047b0cd4b520d8cf0c249ea9ced085de400c0be87ff3" + }, + { + "path": "crates/oa-core/tests/fixtures/v2-coastal-rescue.json", + "bytes": 43990, + "sha256": "3ed53508eb544bbd788832f7bebea2ec31b3d66191f24f45098cc9750ec4c005" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.arcadia", + "bytes": 2450, + "sha256": "305f19d1b79f99f8d4cf627ba2e9ca6836bb7959cc0070227815937a699eafea" + }, + { + "path": "crates/oa-core/tests/fixtures/v3-data-contract.json", + "bytes": 8240, + "sha256": "1b41299c53204e6232397d25a58d5bccdbbc16e770c75e8edb55ce36ccfe58a2" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.arcadia", + "bytes": 1222, + "sha256": "5fa53a26a5ad010c8e59eb56d9b2b52003569e66c0377ffb58840590168ad0c1" + }, + { + "path": "crates/oa-core/tests/fixtures/v4-reviewed-prose.json", + "bytes": 1734, + "sha256": "2808088afcc165f4bd6da5129151143411c997041c3de61d600ab76e05d24a47" + }, + { + "path": "crates/oa-core/tests/language.rs", + "bytes": 56482, + "sha256": "77f6cb2a69976bfb5f60b243b71c07fb0edc7505002c1e9e73fcd6ddf6d9496e" + }, + { + "path": "crates/oa-core/tests/port_anchors.rs", + "bytes": 26893, + "sha256": "efbc0fa18733b1aaac501f9785a4c392fc49c05c45a5778d26e9b5dd951ca6fa" + }, + { + "path": "crates/oa-core/tests/port_migration.rs", + "bytes": 15620, + "sha256": "1801c376a5435ac7efb2a8342348b2c7ffef930bba65deca4acb66b06e0483d7" + }, + { + "path": "crates/oa-core/tests/port_source.rs", + "bytes": 4858, + "sha256": "8d2ddaca4045d330f6eda2591561497b858aad53769a823249d0bea4f5916e12" + }, + { + "path": "crates/oa-core/tests/prepared_commands.rs", + "bytes": 9555, + "sha256": "52cab1c668064777412df0a6a3c20d74d579b33794b4b00525b9f151c06eac9b" + }, + { + "path": "crates/oa-core/tests/process_diagnostics.rs", + "bytes": 1104, + "sha256": "1dbbc590239ef7542be4a2bdc9543aaa64d108417b03dd312a0894e364745906" + }, + { + "path": "crates/oa-core/tests/r3_constructor_ack.rs", + "bytes": 24163, + "sha256": "e5c44676ea0aab97f2e091ddac2f4bc4a4a90f5877f7bbf08b61229600944dd0" + }, + { + "path": "crates/oa-core/tests/r3_migration.rs", + "bytes": 6590, + "sha256": "ebd04120f5215b3956fdc79ee78f6e38734c7d13392f1fc28aaba957912265bd" + }, + { + "path": "crates/oa-core/tests/r3_semantic.rs", + "bytes": 77473, + "sha256": "49c3763eaf2131070a7c46cc23a84f917d292d41610b6404510e5607f8890c32" + }, + { + "path": "crates/oa-core/tests/r3_source.rs", + "bytes": 15311, + "sha256": "f31f60659df3c7ef601650d1cff140589d8729f01a3f65a4a6a6c2272f15c408" + }, + { + "path": "crates/oa-core/tests/r6_data.rs", + "bytes": 41437, + "sha256": "5f7ffb3bc73c3c85423623fe56f8680646dc97040327bdbead9d47a37e80f155" + }, + { + "path": "crates/oa-core/tests/r6_migration.rs", + "bytes": 9174, + "sha256": "81519bc1512f5e069432e9a1cdad04c10216d492df01b4b75b02102480ebd6a1" + }, + { + "path": "crates/oa-core/tests/r6_source.rs", + "bytes": 9519, + "sha256": "88a0aa087a7678e2898ff6057538072ca92072190712f05fff08372cc56ebed6" + }, + { + "path": "crates/oa-core/tests/release1_teaching.rs", + "bytes": 10010, + "sha256": "be179290d871125dc97cfd2a3067c7ac89c8a538fdb0afafc5b825bcdfb2bef2" + }, + { + "path": "crates/oa-core/tests/release_capabilities.rs", + "bytes": 19422, + "sha256": "b92cba2a562e78287ddd7dc3e94557848d17faa0c8a102601b50286b9af48a30" + }, + { + "path": "crates/oa-core/tests/release_diagrams.rs", + "bytes": 7689, + "sha256": "cefcc0582d91266d5de6ee5daefcfe41b606ab971ab235b59034a8e1dbc24b7d" + }, + { + "path": "crates/oa-core/tests/release_language_limits.rs", + "bytes": 5204, + "sha256": "8578da4f76bbd2d0e1bfe295ab1259e2162a3fe3d0d2ba8901915d7a40942806" + }, + { + "path": "crates/oa-core/tests/release_ports.rs", + "bytes": 32740, + "sha256": "506e62beab0c7ec0cece83e9e0c1a810fa1f99285c9e1cab0c77f06de55a40e1" + }, + { + "path": "crates/oa-core/tests/release_routing.rs", + "bytes": 23165, + "sha256": "ae168faaba23769b7ca1fd65f217b9db671726e67276eb5a04e5e72b1447a462" + }, + { + "path": "crates/oa-core/tests/release_scenarios.rs", + "bytes": 41067, + "sha256": "0f5d876badeec653fc7b12610a77d867e9a53582e605253deff8d379da21572f" + }, + { + "path": "crates/oa-core/tests/release_states.rs", + "bytes": 26873, + "sha256": "b1a26acee95eaba7f9920f9da738a1e33606552eb078e6d298895daff7e3d811" + }, + { + "path": "crates/oa-core/tests/release_topology.rs", + "bytes": 17643, + "sha256": "c51dbe5aa5a038f9ed830daa02aa67714472179eb7fd6da5ce36dc6931c8b3a9" + }, + { + "path": "crates/oa-core/tests/review_regressions.rs", + "bytes": 21823, + "sha256": "f154e5c88d35e31512e2b07733a1327ea3e2fcfd1ef8eb23066a4e9573f387c1" + }, + { + "path": "crates/oa-core/tests/route_source.rs", + "bytes": 7270, + "sha256": "1a73fdbac0d0ae0e84d8d67b6ad398e11223958f5dca007f73757c91827decfb" + }, + { + "path": "crates/oa-core/tests/routing_geometry.rs", + "bytes": 31890, + "sha256": "2d8eea4a2bba535ed0ea6daabccdaf87b375828c404baa10518c7bd7fd6253d3" + }, + { + "path": "crates/oa-core/tests/routing_labels.rs", + "bytes": 5530, + "sha256": "dff884d799c3c5c9257210248c55767e294e3cb317c605cbd0ddc9600c216eb6" + }, + { + "path": "crates/oa-core/tests/semantic_artifact_absence_oracle.rs", + "bytes": 4640, + "sha256": "d93eaf9667f4221232168bef0fe9f069e5be1b1f23410bda5dd67010b96e5c8a" + }, + { + "path": "crates/oa-core/tests/semantic_attachments_gui_oracle.rs", + "bytes": 14957, + "sha256": "7f526d363ba05e7ac89faf0fead0796e419c34ee204edf44a1b46c78308b0970" + }, + { + "path": "crates/oa-core/tests/source6_view_contract.rs", + "bytes": 14307, + "sha256": "fa566de53946d40bbbd9033edfeed177180c4dc44afa2f2f4667ffdc622eb3bb" + }, + { + "path": "crates/oa-core/tests/source7_contract.rs", + "bytes": 43262, + "sha256": "cad4f1a135ddbd947bdf3c71d1b44f1e438725095ea8c4d893464bc721c2363f" + }, + { + "path": "crates/oa-core/tests/source_files.rs", + "bytes": 13168, + "sha256": "f0429a0b89a443458ea9af7083c1e4e7411417a7e6b47e618bfbb9266dc164f9" + }, + { + "path": "crates/oa-core/tests/source_identity_updates.rs", + "bytes": 5139, + "sha256": "f52aa38b4d317e532c641ad7463472e4b40e802e60052cf0017556ffc63da06c" + }, + { + "path": "crates/oa-core/tests/source_nested_updates.rs", + "bytes": 16892, + "sha256": "9ac3390936c0d250651ea94a1b8faf0adfb680f061ececc56f2a52909b5ba855" + }, + { + "path": "crates/oa-core/tests/source_project.rs", + "bytes": 45910, + "sha256": "cef58adb16aa4d2527ddc0628aec3e26f0b42b4be70014a7e360fafdac13a942" + }, + { + "path": "crates/oa-core/tests/state_diagrams.rs", + "bytes": 23973, + "sha256": "6b02b387a3ae559cb3ad72de24292a5abcb76aaac0adbab5d2aa5211b22cd2e5" + }, + { + "path": "crates/oa-core/tests/workflows.rs", + "bytes": 33426, + "sha256": "bd843dfddbd5724ec08f657571f887496203056fe2fa5864ef50a168a613b89b" + }, + { + "path": "crates/oa-desktop/Cargo.toml", + "bytes": 842, + "sha256": "f3547b2b924d5db6706c0a8e069014b440ff65315d82fc8aced0360af4b3cdc9" + }, + { + "path": "crates/oa-desktop/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "aa2e4853b25bb7e185b1de49eda42028523c8ebf9599bfe6b7cb2a2b2a688090" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring.rs", + "bytes": 27835, + "sha256": "37c7db33e99dd4adcc5a51f09ee4878af94fd32413359890a541690080885f65" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/palette.rs", + "bytes": 8887, + "sha256": "8f9937cb30b48b83759fd084a8a5990a2daa3920280f1210abe048560241985a" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring/tests.rs", + "bytes": 14732, + "sha256": "ed864839940a1910c97537b6ee151b57ab115ece6f60bae602552b84f9af4a5d" + }, + { + "path": "crates/oa-desktop/src/app/architecture_authoring_tests.rs", + "bytes": 18599, + "sha256": "77eb1a0038d038b989f7ac568760e16fe23c4b8d73cd9fbe32b70817ea23cfec" + }, + { + "path": "crates/oa-desktop/src/app/behavior_lifecycle_tests.rs", + "bytes": 13669, + "sha256": "1af2e290aa259a6af6b0585950cda93fe3a562c7b8be1e938fe1d80747b11808" + }, + { + "path": "crates/oa-desktop/src/app/checkpoints.rs", + "bytes": 29576, + "sha256": "55e5babdcabef767af3bc8be9cc3153b3f5cc0695701b890820ec0225643852e" + }, + { + "path": "crates/oa-desktop/src/app/diagram_selection_tests.rs", + "bytes": 5582, + "sha256": "4fa507adbc23db3a6eb202d809c695ec0ebd6181356930c4fa7528a9a0c7be57" + }, + { + "path": "crates/oa-desktop/src/app/hosted.rs", + "bytes": 25059, + "sha256": "2f7094c078e1d7cbccd6c0d310afa6c400a7e031f12918bbfce65ba62754e316" + }, + { + "path": "crates/oa-desktop/src/app/hosted/tests.rs", + "bytes": 30344, + "sha256": "fa118f64d04b400ccc01e4e67adaf3541975288882afa265a9e7f092cdbda232" + }, + { + "path": "crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53d4a422d3066e07cccb9e8ef2ad08ea95e451b9b7cbef29a27e6634e035ed97" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests.rs", + "bytes": 20067, + "sha256": "b96ddc6bff7f8fde97720e18a92070f891c1340348d2e19c7c9c5a7bda650ce8" + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring/tests/mounted.rs", + "bytes": 32805, + "sha256": "14a4914a1c86e50c395672dab8fe80d7af679a79c9082890b509fad7b7c543d9" + }, + { + "path": "crates/oa-desktop/src/app/review_navigation_tests.rs", + "bytes": 7049, + "sha256": "872fa91479dad5436e139ffcaece270323e9f5daeeb7a4d80c2c2534f48bc21b" + }, + { + "path": "crates/oa-desktop/src/app/save_as_tests.rs", + "bytes": 4113, + "sha256": "f8b915793f1a36fd152569c4781a4bdc17f4af404a8ceb373330053413087a1c" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram.rs", + "bytes": 9979, + "sha256": "f7fd50cc9a86adb794713958348af63afc7c698f74965e13188b7b8a9e735c55" + }, + { + "path": "crates/oa-desktop/src/app/state_diagram_tests.rs", + "bytes": 39957, + "sha256": "612e414b1a2cab12cbfd8fce23745559ff27577700694d53af67ad1f2883ee2c" + }, + { + "path": "crates/oa-desktop/src/app/transition_context_tests.rs", + "bytes": 11508, + "sha256": "47002fb025f3a2f8c53871f9509bfe8611a61db6fb0e1b3125eb6e875425cb1d" + }, + { + "path": "crates/oa-desktop/src/app/view_copy_tests.rs", + "bytes": 7846, + "sha256": "296821c51dcde568e7d717039aa26bfc1b56a9074aa16051007bb351b33a4d44" + }, + { + "path": "crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5e7cecb54bfca64e4462f7881f460900abd925ba2cfe9451ce2b0c78af4bc850" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7758, + "sha256": "9303ed4f74ee0317e4abeab134f7c31f50eac5eb5d85940d771cd3ad595258fa" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7860, + "sha256": "851af1e9b155d6d0d079d9164c5616fadc33e961391a6604a43eabf5158957b3" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "cc58cb3838f546150d22fe4dd83a3ffd52504c279febaeb8d0b4a6a3b8a73950" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 27020, + "sha256": "d84d022574a6f1d69f203d9383502227fb9fc23605c6fb48779ca9f09bee546d" + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "af1c7c1e802d1207ee6c7bd66b703811a250d7b56511512af6c0945c776e68a7" + }, + { + "path": "crates/oa-desktop/src/behavior/contextual_source_tests.rs", + "bytes": 49386, + "sha256": "5f05318a6533503b9d6febf1ec4dd7256f192394bc1b952a7904c8175bd8ddae" + }, + { + "path": "crates/oa-desktop/src/behavior/creation.rs", + "bytes": 28978, + "sha256": "33ae9aaf4ba90de9432870cc8e99708ef39e3638ae8283a9253c333785c91299" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/app_tests.rs", + "bytes": 13181, + "sha256": "e9c5bf2b07357b7a2f679ae0d8f6904c1c1461aaeb94fa0db478b54d4641ae3e" + }, + { + "path": "crates/oa-desktop/src/behavior/creation/tests.rs", + "bytes": 22793, + "sha256": "a0d1daf9fb3920f7e6181a9802d84fbe056cbf0043b9e41dba5fdda687d9818a" + }, + { + "path": "crates/oa-desktop/src/behavior/form_session.rs", + "bytes": 3941, + "sha256": "f5027fb3a5a5599a182e7d1a10dba56dd66eb1f23a4ba4c4a1744c9a6a5b6e61" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "fb1da03e8df05a59253f7fdf34391e30abd8aea427b7d0a12b86b80ff3bf9eca" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/focus.rs", + "bytes": 9728, + "sha256": "73a9ea10ad7312086081b8688396a21ab4251c9faecc2df6d984b702f3963533" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284f5a7da09bdc77d70c6788a091d57927150c796ffc10d786793cff7da4b7c6" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950cf4347a3edea615b1ac71d9a681c32b446b5109fcfd0f03f3ae2b147360bf" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle.rs", + "bytes": 9324, + "sha256": "758d244c2e8de61f515d540baa4d967275f49720e0253c6c9159eb9acd71c859" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_lifecycle/tests.rs", + "bytes": 15430, + "sha256": "01b576993b329810cd78a859b45ac52c66f5a14549065661dd1308196e0b724d" + }, + { + "path": "crates/oa-desktop/src/behavior/scenario_operation_tests.rs", + "bytes": 10087, + "sha256": "89fb71c690e07eac314f7a5e05862d5c1e2f9c21039673bf22c4ac894d21c977" + }, + { + "path": "crates/oa-desktop/src/behavior/source_draft.rs", + "bytes": 18031, + "sha256": "08190823090bd8b0305809099abadcc0de431175f7044f1d5abdceda52612ee1" + }, + { + "path": "crates/oa-desktop/src/behavior/state_canvas.rs", + "bytes": 93561, + "sha256": "2e2ad57b86f4cc1cc0c4c4c230571ef19bcc2f052001a8bdcc44925e5103077a" + }, + { + "path": "crates/oa-desktop/src/behavior/state_edit.rs", + "bytes": 35736, + "sha256": "cb2289e727163626152fc2f11195c6ee824851cb9442c57496da97fe7aebfb7c" + }, + { + "path": "crates/oa-desktop/src/behavior/state_menu.rs", + "bytes": 7447, + "sha256": "cc87c5caa177e114e4d9a7f7367e37ec2b0d1e1c16b0b99709b05ea0e59cf894" + }, + { + "path": "crates/oa-desktop/src/behavior/state_view.rs", + "bytes": 6988, + "sha256": "1eb892a9139ecb0671784c879ee5114f27e598e5b3589f5cdad0abffd5909f24" + }, + { + "path": "crates/oa-desktop/src/behavior/states.rs", + "bytes": 26986, + "sha256": "8986ed9492960e20f1254e9dfe3ff91d79ce9fef66f3bda738f9933278043954" + }, + { + "path": "crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723e51a7da4eb224d51c1659ebbfa4dc8b11557bc8bf70f6e3e1649bad24449" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual_routing.rs", + "bytes": 16731, + "sha256": "d914b0e82aa8d253fe64be2ef74443d1fd2e6cec26d1cf30620a57646e1d426c" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309b8978ec2586ff4614236c2f624af6d934d117363e917b8c1cf9f2b1295eb" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733f8badafb9ba87e3f861f03f78809450eee72cc118341eb03b24904790144a" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "a990947cb39b2fb15c21dfbc95ee16f92bf6be749d3872c6904762b9bde37089" + }, + { + "path": "crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "b2d502c11426bffb8763515f2a56aa4c9f8e203bb36d65eb34f81c123fc406a6" + }, + { + "path": "crates/oa-desktop/src/connection_edit.rs", + "bytes": 5301, + "sha256": "020581e077f82b43c936b623cc51733b02b88428df4e5f25df74b9705819fa37" + }, + { + "path": "crates/oa-desktop/src/connection_tool.rs", + "bytes": 27023, + "sha256": "649da93677d52dae495b0e19bc32f3122ab05a6d84de8402578ff5ce3f6ee1d3" + }, + { + "path": "crates/oa-desktop/src/context_menu.rs", + "bytes": 52955, + "sha256": "629354b34dc232763700251f2695b4d4e53d6251c0b1432ecc0dfe424f7dfeac" + }, + { + "path": "crates/oa-desktop/src/data_values.rs", + "bytes": 23877, + "sha256": "e1deb1cad551f0a9fdef9653aad257cfb36e6d9c0f93b6245061ec3fab8fd365" + }, + { + "path": "crates/oa-desktop/src/data_view.rs", + "bytes": 38871, + "sha256": "158b852be5d3db450aa5cda8b68a35bd96bddce1d8d053a7a23582095c06362d" + }, + { + "path": "crates/oa-desktop/src/diagram.rs", + "bytes": 105256, + "sha256": "4580b170e264d14e5cfa874709f6d2e076a05155bfbe1809336a9d05f75e98ef" + }, + { + "path": "crates/oa-desktop/src/diagram/lifecycle_tests.rs", + "bytes": 9316, + "sha256": "d0e72cc0574657d5a31c3ce307b58401d0ed77eedccd1aa02377f55ab0bc03f0" + }, + { + "path": "crates/oa-desktop/src/diagram/pointer_tests.rs", + "bytes": 11833, + "sha256": "10ca5049034159205ac8f22805354f308b0ba0596f331568c26cd1393bc8cedc" + }, + { + "path": "crates/oa-desktop/src/diagram/port_hit_tests.rs", + "bytes": 5624, + "sha256": "d2d0647629bf725b4dddfc211f5a27d495ed093a79af87cdae6c196f09a97b93" + }, + { + "path": "crates/oa-desktop/src/diagram/port_labels.rs", + "bytes": 13408, + "sha256": "28782983b82e4fca5dd4410ac8bd562359943b7010c7b24ecc42f0c7ad7fb63e" + }, + { + "path": "crates/oa-desktop/src/diagram/view_options_tests.rs", + "bytes": 2815, + "sha256": "cf0c98a7715558baf17b59f67a80556f98ad30262b4434635e617c6601775433" + }, + { + "path": "crates/oa-desktop/src/document.rs", + "bytes": 31080, + "sha256": "2ecc2cd496d4a83fd7a32b9dc68e49f0f1cb31a06263dfd6939b4764da5b73e2" + }, + { + "path": "crates/oa-desktop/src/document_save_as_tests.rs", + "bytes": 5781, + "sha256": "83dc243a25f4e97674ff0d86c794a490cc923e98f1f9fb05bb038f622f11e20b" + }, + { + "path": "crates/oa-desktop/src/explorer.rs", + "bytes": 14764, + "sha256": "fa01c59219564ba9ae5553abc2b6f94e9849f9148d5812ef99b40318fe5dcf9a" + }, + { + "path": "crates/oa-desktop/src/explorer_view.rs", + "bytes": 13773, + "sha256": "a720d7484bcc3a24729332a00e005d1cc6f5f945a9cc3965134e0024dffb581f" + }, + { + "path": "crates/oa-desktop/src/forms.rs", + "bytes": 105544, + "sha256": "ea124f479fcd2d3978ac3cb8eb81fec4a7aae5e4f4414c4bdd54849e954dedf9" + }, + { + "path": "crates/oa-desktop/src/forms/context_fields.rs", + "bytes": 10831, + "sha256": "eaacd474f6500dd344d5a8b63ccb1d1dd86fe2aea78ab708afa8a80d62ce69bf" + }, + { + "path": "crates/oa-desktop/src/forms/data_fields.rs", + "bytes": 49285, + "sha256": "a540a0d451bff56a96b1d28643b2e8c0649f3846350c25c5b154729c0538e77c" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields.rs", + "bytes": 27414, + "sha256": "e7100474cde18bdfe95562bcb3c055f29b4b15aa64788a578194464aebcd046e" + }, + { + "path": "crates/oa-desktop/src/forms/history_fields/tests.rs", + "bytes": 33950, + "sha256": "16d3c308138c063f45986d53c621ad83c7dc2ce853edd31b7204e2939a382df9" + }, + { + "path": "crates/oa-desktop/src/forms/port_fields.rs", + "bytes": 30554, + "sha256": "3fe684d651d5be6203fd9db3979c772913e1af5ac1514e67be386ffd88dc8127" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser.rs", + "bytes": 12130, + "sha256": "b60af363be333af048859e51c28e8c989cb5c95d696cad16c8eec15f78d57a12" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/adapter.rs", + "bytes": 10447, + "sha256": "87ab4bc3bf27fd4b5a458791cf62c7ba8e44e9ebf846d8b851b8b6ffb454cd64" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/tests.rs", + "bytes": 27835, + "sha256": "8e98f8a3fcb814d4a201cc85b3db87a689fd3d2ce487f8a2fe312fc296f93377" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger.rs", + "bytes": 13336, + "sha256": "34d0acecfe6bed193970aa473cc3313984971264b86d7b362a6b84c23d013c0a" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/trigger/focus.rs", + "bytes": 9923, + "sha256": "f1e79f4b03b2f9fd1cdc335054d4ae177dc9e759b6ea3d334b2bcca39b57191b" + }, + { + "path": "crates/oa-desktop/src/forms/reference_chooser/widget.rs", + "bytes": 33804, + "sha256": "d66cc17af7a48bb8598be683f084bb08fa7af7304f9636f0a1bea94c5cf6c727" + }, + { + "path": "crates/oa-desktop/src/forms/view_fields.rs", + "bytes": 24987, + "sha256": "5184d438f0dfd4e29f284ff048e19da73e71be1f24a6e9f123c7e922aecb85fa" + }, + { + "path": "crates/oa-desktop/src/guide.rs", + "bytes": 115183, + "sha256": "5432f56753c8f2614ea053accf00cc095b9c591633959c629c5c80936d05e4da" + }, + { + "path": "crates/oa-desktop/src/highlight.rs", + "bytes": 2751, + "sha256": "da7c254fff7052e5fd9ec7469f473b91a819406028d7c304f6e22c4169561600" + }, + { + "path": "crates/oa-desktop/src/home.rs", + "bytes": 17587, + "sha256": "f03c1071334894f586a5a7c86026602a694db3c2faf902822e1dbe5156a859c2" + }, + { + "path": "crates/oa-desktop/src/home/tests.rs", + "bytes": 8167, + "sha256": "3b03ccfc97dfb75954e1e0ed568bb87a632295c07b96cedc05ed381f87b378f3" + }, + { + "path": "crates/oa-desktop/src/hosted/client.rs", + "bytes": 21912, + "sha256": "54b24b4b50f8d87d7c92946ea5a2cff4c98760e70555b3ce3d90be27115e95d4" + }, + { + "path": "crates/oa-desktop/src/hosted/client_tests.rs", + "bytes": 23009, + "sha256": "3ef70907ca75e098d631c41b90badd4cff1e1eb1b3c78e15a9f811c13ba8ffbe" + }, + { + "path": "crates/oa-desktop/src/hosted/mod.rs", + "bytes": 3744, + "sha256": "1b75e11c80fcb38fd20bb3487e3f88729fdac73e471c5ae64da28c249f77c63b" + }, + { + "path": "crates/oa-desktop/src/hosted/view.rs", + "bytes": 9281, + "sha256": "fc0c7b7ce3a5f05c49713df6661067751f81e810af11c614ca265ea9490e6463" + }, + { + "path": "crates/oa-desktop/src/main.rs", + "bytes": 1511, + "sha256": "cb0a919fd0df0924591283da5b55d64d525d8461640441509350be840235e51a" + }, + { + "path": "crates/oa-desktop/src/model_menu.rs", + "bytes": 34680, + "sha256": "12994a5433a91f8295a59d4bf40f35f1edb8d8dbb497a11eae8760299627f1d4" + }, + { + "path": "crates/oa-desktop/src/navigation.rs", + "bytes": 8839, + "sha256": "71355a12e1b4791106426d83fe5ebf0b29bce8e1dc594f0655ab85695469669e" + }, + { + "path": "crates/oa-desktop/src/port_edit.rs", + "bytes": 7809, + "sha256": "70b99f577c4fdeda8780c8dc1ee7b1639fd77aaca06658f06d725139fb3efb6d" + }, + { + "path": "crates/oa-desktop/src/port_preview.rs", + "bytes": 35855, + "sha256": "069487c4ece737d52e8ae4a84dcfb132236aca6799e841c91c8e7244320cf871" + }, + { + "path": "crates/oa-desktop/src/port_workflow.rs", + "bytes": 40582, + "sha256": "ccc15f97c3272145a67bb5857b4faeaf7437561bd61912fa27ab26e8824146a0" + }, + { + "path": "crates/oa-desktop/src/recovery.rs", + "bytes": 21041, + "sha256": "57e22e5fbb0e994e64ae03e69450271fe9f04a951db1670c2a018abe99db4489" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/README.md", + "bytes": 1587, + "sha256": "a90002df1e087d2dac7be26926864243568a0de7c4103e4927435b34178e8016" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/profiles.json", + "bytes": 473, + "sha256": "2c12ae96b50e6da5d60ce2b7fa00204be71dfbe2fd51311fc337a0de6a4332ff" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-bundle.json", + "bytes": 80974, + "sha256": "12715995e6f7f35414aea53b3ad720b9b31873ffdeaf4444231db1a5ac3c463e" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source5-model.json", + "bytes": 86191, + "sha256": "5535a706df9ec21715e8b6a5aa6dbd79636ae461450aee61305559016167612b" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-bundle.json", + "bytes": 85286, + "sha256": "ea88890a33f47d67dfb06d6dbbcc179058cf28dad76ae7d615c00aaf30dbbbfe" + }, + { + "path": "crates/oa-desktop/src/recovery/fixtures/source6-model.json", + "bytes": 90629, + "sha256": "5bb8db0b6394276666323483f977ac3586f88554c81a148c6d508fca0ef4576f" + }, + { + "path": "crates/oa-desktop/src/recovery/source5.rs", + "bytes": 6956, + "sha256": "0f0a763f0c98c2c908d4c2b95d8a4c042b6a255ee1cc16260e5404669691f642" + }, + { + "path": "crates/oa-desktop/src/recovery/source5_tests.rs", + "bytes": 12146, + "sha256": "cfd17a742f1f27b523fce7cd9309f0a07a30413395f6c58659fb91d02834a7a3" + }, + { + "path": "crates/oa-desktop/src/recovery/source7_tests.rs", + "bytes": 18213, + "sha256": "7ee61de66c75bf949489ba51ddc9677e2b4efc68801cf27909c9b894bfdc9288" + }, + { + "path": "crates/oa-desktop/src/route_edit.rs", + "bytes": 2941, + "sha256": "457ae59c2761a43a8620b4d336a7cf0f54b965997110ecaf968756a88b6b0374" + }, + { + "path": "crates/oa-desktop/src/semantic_review.rs", + "bytes": 3432, + "sha256": "8dd1e93783d63a663c7e56c28775a5c389cfd7a6b562013d6a95c891afa21ed8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts.rs", + "bytes": 24201, + "sha256": "a79236a8150597ff808597e59cf4f1e9e61c74c4901d9c8cf42d3427bb52a9a8" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs", + "bytes": 11034, + "sha256": "3f255d04e4bffa5a467b75e3cbf6f7149aa3269a02f2a89fbaa1db0e5df9ae62" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_tests.rs", + "bytes": 20798, + "sha256": "04d0dd2ac453705185663f705282aa4a3a31433466a5be305764248da1a03c32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/artifacts_view.rs", + "bytes": 6447, + "sha256": "2100ff73565765d8293c84f6ce2d8e566610c1524c50e32ce3c37dc33b6686e4" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments.rs", + "bytes": 18160, + "sha256": "de0ec0c740895b160ff5b847332c087c3f1a48931583464069470cc61315ed59" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_tests.rs", + "bytes": 29389, + "sha256": "8b26be0324e0f33b1e7cf7af340350df8ed98963002d6da8589ee0e6f6900bea" + }, + { + "path": "crates/oa-desktop/src/semantic_review/attachments_view.rs", + "bytes": 3032, + "sha256": "932f231554d77c7cb119d33bb06b9879db5e12127aed187e67013e77b6458e78" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch.rs", + "bytes": 34485, + "sha256": "a462eb627ae19348072775cf8e60b17d62f6b682b74cc413906d493fba1f0b3c" + }, + { + "path": "crates/oa-desktop/src/semantic_review/batch_tests.rs", + "bytes": 27546, + "sha256": "b67c4d7dcabac710cf3d30730a8db323180a558a415e2d117c868cdfa4804619" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose.rs", + "bytes": 27155, + "sha256": "c6adcc9882dde65e9f04f6eda7f0c855c5cbb285cf421123029169112793ce9b" + }, + { + "path": "crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs", + "bytes": 9809, + "sha256": "440daf8d43278ef981c620d1818d0daaed8fb83193b8e8ff67b2e42fbf882938" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructor_tests.rs", + "bytes": 54132, + "sha256": "85149521ad7df62918f6f2fab6da2b3346c342ab4bce7100de3f6206c1e81096" + }, + { + "path": "crates/oa-desktop/src/semantic_review/constructors.rs", + "bytes": 12258, + "sha256": "0982163439b06a42d9af1643530364109282e63f05b323a35a76d8241f3caa32" + }, + { + "path": "crates/oa-desktop/src/semantic_review/history.rs", + "bytes": 3211, + "sha256": "df038c2464eab8093a983f3104f0d04ca8f602010a50e1d3eeb8e53f2309e201" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed.rs", + "bytes": 14175, + "sha256": "bdd6c0fdc7108056bc5d7433254503449eed2209813680e717ead24b9d0ce834" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows.rs", + "bytes": 13222, + "sha256": "4b7bf25806471ad22d89253b9af8000f5acff53b4cc0aa1c1a83138e7b270eca" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs", + "bytes": 30804, + "sha256": "1a3b09e7ed100411cdf8043f9a78defc664a873ee0201117709185c201a5c57f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/keyed_tests.rs", + "bytes": 31163, + "sha256": "9dc9b989e5a9d408427ca1baf86b2454f9dd3baf40a551c70ca2359dc18a1227" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind.rs", + "bytes": 789, + "sha256": "4ed19952ea511d1b3226564a138e3322f994371d6b49794c14bc3689d1393e7f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/message_kind_tests.rs", + "bytes": 23424, + "sha256": "5fcf9930e949aebeb12eab0327264f32a2388d62dbf098bd2365711e22623c52" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered.rs", + "bytes": 9705, + "sha256": "6a1ed894d1b9d0110bc784a75c49444f1d14f1f74620d701f22203b2d8d0a813" + }, + { + "path": "crates/oa-desktop/src/semantic_review/ordered_tests.rs", + "bytes": 22383, + "sha256": "5c8ea3a69e3da3b94d13f50f12aa7dc53246ccba25ffcace9552a0eb76b0bf36" + }, + { + "path": "crates/oa-desktop/src/semantic_review/recovery.rs", + "bytes": 28769, + "sha256": "88e69044d90c481593bab7cc125f0401c05202fccdde07698cec9483947fe8be" + }, + { + "path": "crates/oa-desktop/src/semantic_review/row.rs", + "bytes": 38159, + "sha256": "b08fdc503c6c4d2ff99adac7e18c9baa861bf86155e00b55d1e492430980d0f2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/scalar.rs", + "bytes": 15153, + "sha256": "9ad202174934dd9a63fbf9cba43135aead571db60846c8b7b1d3019a11df26c2" + }, + { + "path": "crates/oa-desktop/src/semantic_review/source7_tests.rs", + "bytes": 11665, + "sha256": "5e8221d77b068fa30c1571988448cb8829711735881d810f3d333cbc95d34a9f" + }, + { + "path": "crates/oa-desktop/src/semantic_review/tests.rs", + "bytes": 32480, + "sha256": "077ec6501dfaab0b05ea3987943b204f08352952296d313555051d998ccd232e" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view.rs", + "bytes": 57626, + "sha256": "7cc7dd213ab01dadad5debaee868c30a1099466602869a799ebdf9b680e8eaf0" + }, + { + "path": "crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs", + "bytes": 4737, + "sha256": "6c766b29c8664d15cb113d2cc71bba9b60ae4ccf7e44c9c5d3c41e1dc4b7ed90" + }, + { + "path": "crates/oa-desktop/src/source_navigation.rs", + "bytes": 3938, + "sha256": "c1da10882f35f8eb0675e68c642c9a209dde09679cded414cfb9903a4e49ec77" + }, + { + "path": "crates/oa-desktop/src/source_tree.rs", + "bytes": 21816, + "sha256": "4d55e0110a1eb128a5d26c28f3aae5853d79b7245812b7c82bd275f38440a5aa" + }, + { + "path": "crates/oa-desktop/src/source_workspace.rs", + "bytes": 15522, + "sha256": "30f63a6b8d65a38d66061f90d864b5746cbacc0fbd65abc028a8f3071c47d358" + }, + { + "path": "crates/oa-desktop/src/style.rs", + "bytes": 3015, + "sha256": "05d61bec16141612ccafa0988e8c4827f41754509efdb3b961eab69127ded896" + }, + { + "path": "crates/oa-desktop/src/tests.rs", + "bytes": 58557, + "sha256": "b017218195eea2c96aba12eba8a99a38945c8ca910a652e272d34883bdb98691" + }, + { + "path": "crates/oa-desktop/src/view_copy.rs", + "bytes": 9043, + "sha256": "5630f31c942e1226aafd67eabdace6a5c5494875bdc6f7f077019e8110ba41a9" + }, + { + "path": "crates/oa-desktop/src/views.rs", + "bytes": 48347, + "sha256": "7148e01e8479c2294093159969bc33b9ff0cf916799d481681811c4de5c60e53" + }, + { + "path": "crates/oa-desktop/src/views/traceability.rs", + "bytes": 7501, + "sha256": "4c89dcf35c25e737d8cce4d543a5989ca104a5678e6cf40df47e457773d2ef94" + }, + { + "path": "crates/oa-desktop/src/workflow.rs", + "bytes": 82963, + "sha256": "d9e7b11a86fd3ec79d91f0c9bc9cebd5a2043cdaa7261622454fb7730ad8e060" + }, + { + "path": "crates/oa-desktop/src/workflow/communication.rs", + "bytes": 11084, + "sha256": "34d121ad97706012722e4bb6210072ecc3a5b4718b0ceab0c3d0f14d4cd780a1" + }, + { + "path": "crates/oa-desktop/src/workflow/recovery.rs", + "bytes": 9859, + "sha256": "0849b0f34dba5e8761be5fa05784d134ac74e4e9db395d6b9b7d6cff71e3f3dc" + }, + { + "path": "crates/oa-desktop/src/workflow/transition_tests.rs", + "bytes": 8553, + "sha256": "589b1b2dbadca74a52c2cfcfd4fe620bd4790abd113ac02182e91616c52d793c" + }, + { + "path": "crates/oa-server/Cargo.toml", + "bytes": 778, + "sha256": "527ff5e15010a8b560a6659aa1ae4bf0c8df0d9e019f03c47529ec6a750c0982" + }, + { + "path": "crates/oa-server/build.rs", + "bytes": 45, + "sha256": "ab1be6ae1fd8bf3ebaeef6348347a9d5e666bde693126f199e041659e0b094a9" + }, + { + "path": "crates/oa-server/src/error.rs", + "bytes": 5610, + "sha256": "93817f1502386e4662c96ee1c7d1c4afc4e33bda6fd64e46c86c8e1afd5a9368" + }, + { + "path": "crates/oa-server/src/lib.rs", + "bytes": 7743, + "sha256": "20bef350350ed6339588b23af684abf745f97916c7edf3853ee042abe687e664" + }, + { + "path": "crates/oa-server/src/main.rs", + "bytes": 4163, + "sha256": "d59a6a2b5c7034bd2e00a516d2d49abb16391bf0316c1c357ddafb9c82d2d1e3" + }, + { + "path": "crates/oa-server/src/preparation_http_tests.rs", + "bytes": 5523, + "sha256": "454b90d0055d35644c8d990cd7576ddf675a9b271ca07fe9b0efce25d8ddbc07" + }, + { + "path": "crates/oa-server/src/routes.rs", + "bytes": 46053, + "sha256": "3f9a0c22043dbf28f7a8e587d2fc3abf869f76f4d23a46fe0aeb9c4392454a4b" + }, + { + "path": "crates/oa-server/src/store.rs", + "bytes": 25524, + "sha256": "37dcfb845b7993d5bced2bcf25fedfcd2a9c20a748f9315afe6cca1f41f89ae5" + }, + { + "path": "crates/oa-server/src/store_preparation_gate.rs", + "bytes": 4440, + "sha256": "427c390da3c0281ba1fe115a93ea90800e3b45fce6bcb9185e71982ddd885fec" + }, + { + "path": "crates/oa-server/tests/http.rs", + "bytes": 71892, + "sha256": "50daa6a90e96ad6fe3c6a27df2eaebe486270599c203892d09769c547a194975" + }, + { + "path": "crates/oa-server/tests/http/communication_limits.rs", + "bytes": 5790, + "sha256": "ba5474dd8e494a0176fffb4548c8e3deb2081b15de33bfce82394c764e0a2c16" + }, + { + "path": "crates/oa-server/tests/http/data_contracts.rs", + "bytes": 25949, + "sha256": "8cd456db83f301baca380acf025a5a99db4edf7d8839537dfbd02c4307e10e6b" + }, + { + "path": "crates/oa-server/tests/http/port_anchors.rs", + "bytes": 15086, + "sha256": "7199d0a208ddad9e1518b6e3fb390d64da111716d7505ba5c995deae19ad1258" + }, + { + "path": "crates/oa-server/tests/http/semantic_transitions.rs", + "bytes": 22832, + "sha256": "856d1c9b633ffb581e3e631f4e5a607789a02181dbf060667f208a569bfa4920" + }, + { + "path": "crates/oa-server/tests/http/source6_views.rs", + "bytes": 13023, + "sha256": "867bccda0e030eeeb9976b4ceeb47b21ab5044d700278022d5fb495bbb64ca07" + }, + { + "path": "crates/oa-server/tests/http/source7_contracts.rs", + "bytes": 11000, + "sha256": "0e0a467186da7263c0754890aff69ebd0d92d36b5224e4865bb6854b18dab7e8" + }, + { + "path": "crates/oa-server/tests/http/source_projects.rs", + "bytes": 30907, + "sha256": "8ff580f317ec267d1cd72ae14d8638f74a77cdf99ca4c78f66f4290c2d77460a" + }, + { + "path": "crates/oa-server/tests/semantic_http_contract.py", + "bytes": 7251, + "sha256": "4737ff94f10761f94151a694414c5c62737e0391ec820bca132830652bc4cca2" + }, + { + "path": "crates/oa-server/tests/semantic_schema.py", + "bytes": 25111, + "sha256": "cacca116c42ffbc097f1419c0f89921fa0dfd3faa864d46820ff744cfe6967f9" + }, + { + "path": "crates/oa-server/tests/state_representation.rs", + "bytes": 12241, + "sha256": "6686c826f28248ae4b63ec3f2112e193d4a347724b87974483217fcfee49cd61" + }, + { + "path": "docs/acceptance.md", + "bytes": 3463, + "sha256": "32ed3e4fed63f5c464e0937221f401d71623b276dd70f083531ac6ccebbf9050" + }, + { + "path": "docs/api.html", + "bytes": 72515, + "sha256": "a06126cb741da57e9c06e83fc4137ccde0de05562673546bc9d405878f51ea07" + }, + { + "path": "docs/api.rst", + "bytes": 41929, + "sha256": "ca7d310138c3b4d429d80a1d03d1ac8d4a9296069e205da573034256d10438a7" + }, + { + "path": "docs/architecture.md", + "bytes": 10974, + "sha256": "a50bd1645222ef3e574fa07a6ab9aa213431ee8833c6c0158ad91ce0f799543c" + }, + { + "path": "docs/language.html", + "bytes": 175111, + "sha256": "67dcfa3477b48a0b07298ec81741840cc3ccff12c7d58164873d746d41d611c8" + }, + { + "path": "docs/language.rst", + "bytes": 130753, + "sha256": "ac6b1d982c55f246de2f3c844b24b874599a3c20cfbd7ed22efe4561ac622b83" + }, + { + "path": "docs/linux-packaging.md", + "bytes": 8671, + "sha256": "41a1bb1095801df37a1411341c5791d55ea8c365739314d599a60269ac4bc7a0" + }, + { + "path": "docs/openapi.json", + "bytes": 346920, + "sha256": "bc5825a3182a4df5f6710a80262f4a09b4c15f0e4cda7ca74e1746ceca81a89b" + }, + { + "path": "docs/research-v2-architecture.md", + "bytes": 40445, + "sha256": "1b3809f2dabe6b1805dbf3d3f2625c471cd21356c7bd603916466a8c4ced8090" + }, + { + "path": "docs/research-v2-semantics.md", + "bytes": 31535, + "sha256": "b7ec3677300248926a88b84da3e7ffe52f5b73524c7712c927a0e951569db04f" + }, + { + "path": "docs/research-v2-service.md", + "bytes": 36209, + "sha256": "4828df5bc679e7c71f49b4b85c9e7dacbec52f46d416fed48af70e08886d4e4e" + }, + { + "path": "docs/research-v2-ui.md", + "bytes": 3245, + "sha256": "894e721b59a8ca85f0d95b6706ed5f130a0372def460cf33aeefc9e5e38cde0a" + }, + { + "path": "docs/scope.md", + "bytes": 16260, + "sha256": "58b88a7f0b1c8a79edf23c375a141d7b1b821bca4f3212a80e29db5ea3fba796" + }, + { + "path": "docs/server.md", + "bytes": 23361, + "sha256": "4eb24937d424ba8b384e1734e02a6f8e963ae11d2715889c92b14b39ff2e4a48" + }, + { + "path": "docs/source-cli.md", + "bytes": 3374, + "sha256": "34f12b72cc6d6ead4f47c7818c1897db592f8ad493c70776db2ab48c24d8f051" + }, + { + "path": "docs/v2-interface.md", + "bytes": 11807, + "sha256": "c7fbaf0ebd3fc9f6c11f818db2b0b9d68a33f580cfced22bde83ce08ae790bc7" + }, + { + "path": "docs/validation.md", + "bytes": 6505, + "sha256": "f46ea950ed1a54409aa07c4f9e90f39f8e2d00c1dd1b6cb805caa3993845967f" + }, + { + "path": "docs/windows-packaging.md", + "bytes": 10406, + "sha256": "2c852ff7041713ffbff3c951ac98df647c51fb70b0feba9000f7bfd5bc2703a8" + }, + { + "path": "examples/coastal-rescue.arcadia", + "bytes": 33107, + "sha256": "7e183ef9fd8154d3074303f1956cd0d9871c19e3ffc630d333b0bffbd6e30e17" + }, + { + "path": "examples/coastal-rescue.json", + "bytes": 49715, + "sha256": "242f0b031229a87af5c4ebe5d651defe59c191a6b7a69dc0a77f7e203411f195" + }, + { + "path": "examples/legacy-v1-coastal-rescue.arcadia", + "bytes": 3449, + "sha256": "4dbbbdf8bc7e6cb8e0a81dd678069dae585a9e5187c59bc3665e1fdab3f1a86a" + }, + { + "path": "examples/legacy-v1-coastal-rescue.oa.json", + "bytes": 5976, + "sha256": "fd0e9dedfb7adb922dc0575cc2b820d40344d639f1d8052bd70363aa59de9c24" + }, + { + "path": "examples/release1/README.md", + "bytes": 11137, + "sha256": "113cfb1cf38a98b51bff60442d86a35d636a3d80ef206bece7d53d0aae67f2d5" + }, + { + "path": "examples/release1/evidence/repair-check.md", + "bytes": 1419, + "sha256": "f4db17bdca4ecbb1bf0caef01b2da5f1f7f798ab3152b648ce6dd8696a6a68e5" + }, + { + "path": "examples/release1/expected-diagnostics.json", + "bytes": 288, + "sha256": "3700393d76b940cfc076bf52861bd7b1f43370f8eabce3594531bd0bf1fc851e" + }, + { + "path": "examples/release1/model.json", + "bytes": 134520, + "sha256": "ef0ef4006c3e09d5db6e1b621256b3f7d6642f3a7b09cc34607cfbcca830c727" + }, + { + "path": "examples/release1/model.rs", + "bytes": 28762, + "sha256": "15f30f6bde655808fe754665c26d560e36f2a9bad340f346b09137d0d05045f5" + }, + { + "path": "examples/release1/oa/activities/activity_oa_restore_work.arcadia", + "bytes": 1307, + "sha256": "83e73d0a013d5c3e1a4af0d9139ab0cfa362d25a846671fc2dac59181061a9d4" + }, + { + "path": "examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia", + "bytes": 697, + "sha256": "172c32c044ef4b5bc58a1f4635cc81f48dabb4014479b47dce7b25fedf2dbd0d" + }, + { + "path": "examples/release1/oa/behavior/process_oa_field_procedure.arcadia", + "bytes": 1781, + "sha256": "d2552dc5383e2e91cb302a790364e197f663c2edc013a2ab1978789825c9c324" + }, + { + "path": "examples/release1/oa/behavior/process_oa_response.arcadia", + "bytes": 3225, + "sha256": "cf86f46fa5c5d70592ef43f1bc1cb4d968efd8add384ba88036bef6b88aa4abc" + }, + { + "path": "examples/release1/oa/behavior/scenario_oa_conversation.arcadia", + "bytes": 1795, + "sha256": "b25be524e98a35ebac5e4481b7609c027b32baf9605e8097f807560f94a77ca6" + }, + { + "path": "examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia", + "bytes": 1261, + "sha256": "00a1edf2c9707a0eb62ac070c2d02467874f2c9c1f273cc6ddb53d6d65999a1c" + }, + { + "path": "examples/release1/oa/behavior/state_machine_oa_monitor.arcadia", + "bytes": 2022, + "sha256": "447c9ad1359f4a31ecf975ad07c0b08d8e7e03191fb0858e2fc56f377b223cd5" + }, + { + "path": "examples/release1/oa/capabilities.arcadia", + "bytes": 781, + "sha256": "fc8012e5554350b12e1cb80080f8feab80d933c9b33c0f2a7bb500cb7d609936" + }, + { + "path": "examples/release1/oa/model.arcadia", + "bytes": 661, + "sha256": "f7cf914766ea73ec53b85e32d117ea9a9dba52d3e038246863541ae7069b1a15" + }, + { + "path": "examples/release1/oa/participants.arcadia", + "bytes": 1834, + "sha256": "951764c14179f27c10e315acf2ea37d1fff54c77b8441e23addd239e8c715ce3" + }, + { + "path": "examples/release1/oa/types.arcadia", + "bytes": 788, + "sha256": "9d0396fb82dd5733edd8d1cf52f968ca84906927c704f9cf0944a7fae215cf49" + }, + { + "path": "examples/release1/project.arcadia", + "bytes": 1958, + "sha256": "b2674e6bf77cfe0f6f0ba79c81d2e96d92f39c02ca2c6266d80e91723bfb9fda" + }, + { + "path": "examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia", + "bytes": 706, + "sha256": "73928ced61c06cbc08728d7956676a50fd4c05aa5b7f1ff4f29ce27a0b0f2088" + }, + { + "path": "examples/release1/sa/behavior/process_sa_field_procedure.arcadia", + "bytes": 1790, + "sha256": "886d462af83110cbfa55dd232a667ea1446761234471357cbb878794e98e8768" + }, + { + "path": "examples/release1/sa/behavior/process_sa_response.arcadia", + "bytes": 3234, + "sha256": "041785fc22842423822431bd5e6e8d16094ebd0716965e7c765fb8c1d7549e8a" + }, + { + "path": "examples/release1/sa/behavior/scenario_sa_conversation.arcadia", + "bytes": 2127, + "sha256": "97276f5757ee9aa12a0dbd9d3ccd58174b3091162bb8bcce357cec20a45f5d85" + }, + { + "path": "examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia", + "bytes": 1256, + "sha256": "e12fc406b99d60a2f7768aaa0b25ba560e8605f0b8e37f3a7cdd3b8443562b74" + }, + { + "path": "examples/release1/sa/behavior/state_machine_sa_monitor.arcadia", + "bytes": 2022, + "sha256": "05ce29af99424c491c47fe2450dc6f5f8ce995511c01ec7e17bb5855be3c9736" + }, + { + "path": "examples/release1/sa/capabilities.arcadia", + "bytes": 781, + "sha256": "b6912fb3ece439912345fa71724acd462e716a1e5a88203a1058a5c3b2c20a4f" + }, + { + "path": "examples/release1/sa/functions/function_sa_audit_thresholds.arcadia", + "bytes": 297, + "sha256": "e5a04325d52183956dca814dd90f4e56c96c6b7db0f4f202f203ecb49c3e0f4f" + }, + { + "path": "examples/release1/sa/functions/function_sa_restore_work.arcadia", + "bytes": 2576, + "sha256": "fddeb67308051325041fee641fe758ddf43345b7c47b66d433616ad07fdccdd6" + }, + { + "path": "examples/release1/sa/model.arcadia", + "bytes": 661, + "sha256": "d431509251acb859fbb98733b6d18add952495e10a78703fbe8f31e40bc083c4" + }, + { + "path": "examples/release1/sa/participants.arcadia", + "bytes": 4012, + "sha256": "780d2eb08b217b67b55febb3d34846f727b8ee9018df6c5741748647e36b2ff8" + }, + { + "path": "examples/release1/sa/types.arcadia", + "bytes": 1227, + "sha256": "101fc0fbd99f1101514aa7b6da4f8e535aae0bb94c9e2b9e7e6c151135b28660" + }, + { + "path": "examples/release1/traceability.arcadia", + "bytes": 29506, + "sha256": "48a96f543456c9b5a5798fc795594e4119147fe45f11a6f109f141b7f514560f" + }, + { + "path": "examples/release1/transition-before.json", + "bytes": 35801, + "sha256": "2253e6434f741687e22327c37d520ed15c043bc36025498650bc2366bb6a874d" + }, + { + "path": "examples/release1/transition-request.json", + "bytes": 4276, + "sha256": "43b9a22e5f95ad3bc0ebd2478ead0e14406854abc989ae94ded9367b013d812a" + }, + { + "path": "examples/release1/views/diagram_oa_allocation_view.arcadia", + "bytes": 2176, + "sha256": "dfc0eb61e0e2aa5bda45524571fbfa3494f44ce19696f3e15e07438074a07bc7" + }, + { + "path": "examples/release1/views/diagram_sa_allocation_view.arcadia", + "bytes": 2173, + "sha256": "b78b43a26a82efb8fbd5374b09c329fa341f57b29f635d397b9ef8aede71d389" + }, + { + "path": "examples/release1/views/diagram_sa_capability_view.arcadia", + "bytes": 1620, + "sha256": "75e8dc9fb30f25d6f8fd3c6b595593ab4f98958a25798f961e336e0fac557649" + }, + { + "path": "examples/release1/views/diagram_sa_service_view.arcadia", + "bytes": 2209, + "sha256": "b08c84f3ea633e7c78dfdad3272f75415f6256cf3614b6d99bfd4f0269076c9b" + }, + { + "path": "examples/release1/views/diagram_sa_state_view.arcadia", + "bytes": 1931, + "sha256": "5f42f4c605e57b817d8400ed21f2b14bfc4e1592aebe941ff113767504c2b869" + }, + { + "path": "examples/routing-workshop.arcadia", + "bytes": 66431, + "sha256": "dc6b75da72884d399132ed9e300fa5a26b73d802bb526c9af986ae9eeb0a646a" + }, + { + "path": "examples/routing-workshop.json", + "bytes": 100281, + "sha256": "8592714f75b271133f21ce017d87896b6cf91ea0e4837f1c2ed453e21be26b14" + }, + { + "path": "examples/source-project/oa.arcadia", + "bytes": 2142, + "sha256": "5f60642f211b2d352a951092faeeb7503d4fb7f6f7fca99e810347231101d31c" + }, + { + "path": "examples/source-project/project.arcadia", + "bytes": 606, + "sha256": "787f6b67b179ffd221f7e5faf1a4edd74cfd116aa1e3b5e9ed9f22fbff612d8d" + }, + { + "path": "examples/source-project/sa/assurance.arcadia", + "bytes": 682, + "sha256": "ab76044836e4dcd42a82d316846ec87da2108939aa2037640c5bca1d2ecdee0f" + }, + { + "path": "examples/source-project/sa/participants.arcadia", + "bytes": 1231, + "sha256": "1abad579812e77877b45a3d8e3c3e9da734be2d04a0254cc730a5257ebd2dc88" + }, + { + "path": "examples/source-project/sa/reporting.arcadia", + "bytes": 2839, + "sha256": "f0b7443f728912ca163de39b6055876b533a77feaa3219b2d40a2dd65712e058" + }, + { + "path": "examples/source-project/views.arcadia", + "bytes": 3508, + "sha256": "e29ad3f406a6a50d2f0ae2ca2a53ee3b0c18222cd4098704a46d4f48b64ea17f" + }, + { + "path": "examples/text-first/README.md", + "bytes": 1367, + "sha256": "4ec8250d5ed556f1d496e8c85137e973e802ecb11df1a36488411e8cdc5a9f9e" + }, + { + "path": "examples/text-first/project.arcadia", + "bytes": 480, + "sha256": "f441535c623e0cf20e1875eb2cd33b587ed00e078c37af39cb36d89bb5edc19a" + }, + { + "path": "harness/fixtures/basic-editing/README.md", + "bytes": 12012, + "sha256": "3545d9695e7c62e0caa818efd7f4dc426f04876a3626e0f3b1bad606b258cb26" + }, + { + "path": "harness/fixtures/basic-editing/base/expected-baseline.json", + "bytes": 4524, + "sha256": "f239e728d650b0af32bb8f3dcaf6b27c99b9c5041132ac14e51756ea6563d978" + }, + { + "path": "harness/fixtures/basic-editing/base/project.arcadia", + "bytes": 3077, + "sha256": "045cb047d7bfbca72c3fb30db4c81a30ad463d3af7fc51604906ad7fa75a1ff8" + }, + { + "path": "harness/fixtures/basic-editing/base/rationale.arcadia", + "bytes": 57, + "sha256": "c01867c9dc0d40726853119fac3c6ad24a40d8d362a93a8b9d20df04ec2e273f" + }, + { + "path": "harness/fixtures/basic-editing/manifest.json", + "bytes": 447, + "sha256": "d23c21b02064ddb85e051e10e16ff089daba8d9910bb1a695dfabe2db3757734" + }, + { + "path": "harness/fixtures/behavior-creation/README.md", + "bytes": 5954, + "sha256": "9e5fc751424d12cfe76a84abb1da45b2d38947409179451bb90ffe7e7ddc8723" + }, + { + "path": "harness/fixtures/interface-operation-probe/README.md", + "bytes": 2604, + "sha256": "088f0bdb3375deb8f2c7e3b749ca8d210ea828ca4dece69b3871b674e3110175" + }, + { + "path": "harness/fixtures/interface-operation-probe/hashes.json", + "bytes": 977, + "sha256": "f6d3e8aa42f40f64ce2ecba7b9cf112eb9091f3cccde3a592b0434e834cf2a6a" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia", + "bytes": 4218, + "sha256": "c0fe9570684c0061efc4cad65e339fede6961b04ca90c6f324c787bd65e61226" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/after-reset-deletion.json", + "bytes": 7835, + "sha256": "93187f7a1b476a44185725c6b15f2e1767482b6911ab1280144348d033eb51f0" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/boundary-only.arcadia", + "bytes": 4771, + "sha256": "02f85e39957ebe6d56336fa729a5c8b10f51865face9d3b2c9679200571fcfc7" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/boundary-only.json", + "bytes": 8840, + "sha256": "756f9710336e6734ce3592ca774c6f104bba5a37c9e165540eb39326682393d1" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia", + "bytes": 8635, + "sha256": "ac5b629ba5596464f9f8a975b8bc2764e3007ceb7342de3f828d4b18e4f069b7" + }, + { + "path": "harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json", + "bytes": 16160, + "sha256": "78a47c978dc667aa1c45af711f06aa2a0d3c581003013e30c3e02fb0faae2e64" + }, + { + "path": "harness/fixtures/interface-operation-probe/probe.rs", + "bytes": 11274, + "sha256": "72b1b443e25eb20420dec4477b0fb5ef5d1c705e3882c81df88dd578ab96a426" + }, + { + "path": "harness/fixtures/release1-native/README.md", + "bytes": 2564, + "sha256": "5f5ba040fd2df9261a29bcab3f9a7ae27c3683265411ad3dda0b467d5df8590e" + }, + { + "path": "harness/fixtures/release1-native/artifacts/README.md", + "bytes": 4155, + "sha256": "24200c84ccc2ef3e17c029744074e5cea785188f5549ac54e44cf89682643f81" + }, + { + "path": "harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs", + "bytes": 9275, + "sha256": "444f360b78df52c5e42046f77b148781e0c9bddc2a09c0b7bd65c7a4f376cfe6" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia", + "bytes": 7768, + "sha256": "84b22e95c213018612e6e8a84cad68b35a8b495ff3515b0f978c5e7a3d196d79" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json", + "bytes": 7330, + "sha256": "13a6f99ec2df1dd35ee8b2613f183918cd9537eda4a5da085ad52e65c033db59" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia", + "bytes": 1446, + "sha256": "1e0d662ba33071b122ad9206d2d559a8e748c9431eb3ac1b759bde3760ca877c" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json", + "bytes": 2216, + "sha256": "fa6a2e3643c0f909b92407abec3edc897033162eeaa501f8d4616f79d41fa044" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json", + "bytes": 567, + "sha256": "65cb70b0d697a055363ba01bf30bed7e412d43c9978a033549c378d117719ae7" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json", + "bytes": 3991, + "sha256": "ed905267263e7f8a0cd2dbe833196b30118b6a246815216932fae661ad650699" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia", + "bytes": 6837, + "sha256": "e50fc462ec22dbf98011929931dd2489430001c3e6433608685b9d88bceb9acb" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json", + "bytes": 6433, + "sha256": "f27a7b38d59c7cefe58cb36783d3d4dd8d8ae1168d9aed1eb61d343c18758908" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia", + "bytes": 1807, + "sha256": "eba2aec39e6efb405eace4bc8ab0c5563907c26d449e71a4aa750df9f73233b4" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json", + "bytes": 2673, + "sha256": "2ef777f19b94f08b5e5ca2ef727777897e1018f1426eee291408b3c69101f596" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json", + "bytes": 519, + "sha256": "3af7232ed3056ca84dcaf0f266ead3faaa512a0dc0051c7ba68791c20dbae0cf" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json", + "bytes": 1246, + "sha256": "8af21b00b62e620256fbf50e47c6c5836cdb6922d9549cc4b3d86bc03b008073" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia", + "bytes": 4200, + "sha256": "20bb30473830e44228958e78fb7480269cdcc0580c6c90c718da75f34ca5a43f" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json", + "bytes": 4552, + "sha256": "44f4b05db49758dcf344788f57cecc0250fd029099b1c54e4e1e3e972f6cf039" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia", + "bytes": 1808, + "sha256": "b0fd031671c9f755756922ec5c40a0ce3f40164d22c3c389aec9fd0705344da7" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json", + "bytes": 2673, + "sha256": "1acf81e08c51c85aceb14a3efbc39ec680552a19c7824aab4611ac3cf71b9c6d" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json", + "bytes": 610, + "sha256": "a07978f4b0a3ca83e5ae5f35e7b66b26473b24431fc90e5889d755f1816d52dd" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json", + "bytes": 2352, + "sha256": "5ff844a038b0030adb5a83d13d81e60f30627543388f6199b6a0e026fc42ea58" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md", + "bytes": 4075, + "sha256": "a51ffbc45b6c172cbfed3b16c249296e6a28d457fea57393dd958cdcfbafbca1" + }, + { + "path": "harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json", + "bytes": 2367, + "sha256": "3ad8b7428b3ed7585a91e7f5d9d5f4880caccfd0b08dca25d05af523e62e0a10" + }, + { + "path": "harness/fixtures/release1-native/communication-means/README.md", + "bytes": 15091, + "sha256": "015c16010c212af283f9def118c6d263ba65a3bc6e81a750f180624ca69bb311" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia", + "bytes": 6098, + "sha256": "a6976e7c21031e7cc3cb731e20c3075cf89cf85144dc8d3f52930f8521ea196f" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json", + "bytes": 10480, + "sha256": "60c4419415c546f06c9972ac7bb0e530ada7b30a200b59e75ddda231a24f9a3d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia", + "bytes": 407, + "sha256": "3c41459f2b0706a767988738bf8126f76540415bb5940d9c5db19cbe8c18182d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-create.request.json", + "bytes": 1331, + "sha256": "b16e673b29c96b5aed681389ff7669110abe552939e86dd0ec6011c9a45ed860" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-draw.arcadia", + "bytes": 5239, + "sha256": "a511a4d817d9c5c80233bce83e84a7056b7dce7afd9fcc25696e333f8f4df055" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-draw.json", + "bytes": 8424, + "sha256": "7140114926228b1c378c450535f7a70992c71a879a8d2270c49e8d5f04a2f484" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia", + "bytes": 5710, + "sha256": "910e6dfe609467047ce629ec9c626f4e927cada74bfbfa164414a8fe6783ea87" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json", + "bytes": 9868, + "sha256": "567259e20c9f6278776e8fa99b8b2fdbbc9a883ad875c5d39dae1b53de3a2924" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia", + "bytes": 407, + "sha256": "997f72fe9258012df66e88344cf39c1a2cebf2327f604056b67f7f2a8ebcfbfa" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reuse.request.json", + "bytes": 1127, + "sha256": "cb3fe05859cfffec6f8846c19983dc462413bc0ea4f6e19204f09c259919c4b1" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia", + "bytes": 8209, + "sha256": "ec826b9b84616507490ac30e4d57100dd6338dba2f7071e879a8fb58820a85c5" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json", + "bytes": 19735, + "sha256": "ef07e4d2d3fc36004ff01739838281581d9a703c747611eacc01bb9cc672489f" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia", + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json", + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia", + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia", + "bytes": 9827, + "sha256": "86dd8a355f6c9dadfea56d06e5247c9b0f820819d7df17ef5690671c7c81324b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json", + "bytes": 21877, + "sha256": "980489ec2c483d4f9efada61358c1f29bafb2572fd5b804c656b91b8e9bb4f9a" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia", + "bytes": 1488, + "sha256": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia", + "bytes": 2068, + "sha256": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia", + "bytes": 7698, + "sha256": "1abc4c4cac37cfb06ed3b12a115f9e7e6a2d3affd34b25c355309e5ee465fa40" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json", + "bytes": 13130, + "sha256": "a5cf4cb2ef390113dd752f4054d43fb789da2ca77799efb5782805ae8aa2dfd5" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json", + "bytes": 1090, + "sha256": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia", + "bytes": 5454, + "sha256": "b761c4c2af5f7064aaf1f887ffd6094eb6ae834b2c8057ecdbc4aa97c042066b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/cm-transition-ready.json", + "bytes": 8852, + "sha256": "2536b000de361fb131787e32b466b9123d4e98abf00c099ad32f6cb8e7fb530b" + }, + { + "path": "harness/fixtures/release1-native/communication-means/generate-communication-means.rs", + "bytes": 28035, + "sha256": "003521404b702332082e01f2be7676fda1dd7b5bcf220bb81d25120c8984c75d" + }, + { + "path": "harness/fixtures/release1-native/communication-means/hashes.json", + "bytes": 3133, + "sha256": "39b4dc02fc8b074015c59354fd1c1061c25832fde25065c7151f36eb0455e6f6" + }, + { + "path": "harness/fixtures/release1-native/communication-means/manifest.json", + "bytes": 1237, + "sha256": "fb921f42f2c4a4f8a67d1266f17f8c7750b6a2619554b713aa92b17918f0c43e" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40-oracle.json", + "bytes": 26264, + "sha256": "74edca148583f42c269e51f135662f65ef1c9ea1c94516d63e2a5091967dc99b" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40.arcadia", + "bytes": 28947, + "sha256": "00b87736b82c5afd33b311df7aebe9a6c8c51b6de66221181b5ea8bc8ca221a3" + }, + { + "path": "harness/fixtures/release1-native/dense-40/dense-40.json", + "bytes": 54938, + "sha256": "5dd4f22453330cab3abc059881660190e689a22619853f77cf6d0b52735d97ad" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments-native.md", + "bytes": 2624, + "sha256": "28906924b05f75bbf432389517aef65f1df474d2f2bff79a3586564f7f7328ef" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.after.arcadia", + "bytes": 6795, + "sha256": "9d084dd75a0251d349cc31f9ee98df58d0bc76859dc3cb1503f242e2f759888a" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.after.json", + "bytes": 9618, + "sha256": "f09be41afd85c18fd52fdf997e9b81c4b7cb2c41e78d167abcd14caea8110333" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.arcadia", + "bytes": 4990, + "sha256": "8740f27be99624013d9be876639c0411e1b8c14f0f9a843f43dc71d769efd79a" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.hashes.json", + "bytes": 963, + "sha256": "00fc9e2c709bdcd9ee98012176cb0edb635c112153d963cd169bf4bf69a9e010" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.json", + "bytes": 8146, + "sha256": "9c188f2890630c7feec2be719a7104af9690da7ea836df18fa78da3ff2acfc1b" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.manifest.json", + "bytes": 1045, + "sha256": "46b40f221ffad614733b615208814ace63947e96b80bbfdec63853ccd4f4e8c8" + }, + { + "path": "harness/fixtures/release1-native/semantic-attachments.request.json", + "bytes": 3075, + "sha256": "b57f656f0b9cf2c595e83150c683301909e20f15684d35d108d3c57ee0dc5909" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch-native.md", + "bytes": 2637, + "sha256": "dddca2b85f892b05881c75ef3cd62437396cc086a09909b621e02bad0cbe56ff" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.after.arcadia", + "bytes": 3134, + "sha256": "d3315bba762395205ae761e6e64029e8b4070c15a1299c6114075581db7b0974" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.after.json", + "bytes": 3927, + "sha256": "8f36119c7e893a4af374b13b85b1c2cb3df0196df8d06a551386628fc8b8646d" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.arcadia", + "bytes": 1787, + "sha256": "5e1f433e04621424edac43e53411624059e873f88dd531bfdb39093953485ed7" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.hashes.json", + "bytes": 933, + "sha256": "1bd87700b04e4f169ab5f8aceaa3b0c3ec3a1cc43e1bce135a1cee506fc75d38" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.json", + "bytes": 2816, + "sha256": "1972242d2a8677d004dc3af1081feb292de65fe26128d877dded8661f9906619" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.manifest.json", + "bytes": 647, + "sha256": "1e440efa02e2ccc27585d9a49c7864d53e3e3e7645ccb219f040e00d95331b8e" + }, + { + "path": "harness/fixtures/release1-native/semantic-batch.request.json", + "bytes": 2197, + "sha256": "71f23474614aab90398ce24f00f2cf61d6cb9a3a34faa3e12e0c8104e0a71073" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-repair.arcadia", + "bytes": 15635, + "sha256": "e3ce0347746584f503b825a8b6bd23a7eebf0827940112ef8f36d500c1d0a043" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-repair.json", + "bytes": 25736, + "sha256": "29736f2a619f7f2d0e795229f6e8c3645a1b4eca6292043cd604aa01b1ec9170" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-take.arcadia", + "bytes": 15631, + "sha256": "9ab573c92be66e3bae659dd826923ba6066979b1651edda7bd469056632cd77e" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.after-take.json", + "bytes": 25736, + "sha256": "828c990c6129692cd31c7f6ed0dd6c952f9cfffdd90bc80171df82d4d929afba" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.arcadia", + "bytes": 15617, + "sha256": "49214be2ca9c8bb6195c3b39737c053847021839f70f0bb2921d35a1fbd2ac83" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.invalid-request.json", + "bytes": 2225, + "sha256": "8b02d4739b1be2c8a64da76423b4a6cae7d89c60f4d30723b94f1da3779cbcca" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.invalid.txt", + "bytes": 78, + "sha256": "aa536e18bc65325163e9b645a4ad031ab599fcb05948dcf741b725d0f0c8b602" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.json", + "bytes": 25722, + "sha256": "995dd72f5c1f07f998233ae9847e337cb82de0c2a9053d3905be021d5f8124b9" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.manifest.json", + "bytes": 1059, + "sha256": "ad315359bfd68ce0d265976dc9e4ea6a502ebbb87d7339dd1af5ef27dd01e433" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.repair-plan.json", + "bytes": 15016, + "sha256": "27bf316ad66ffa91f6323e75b0c280baf5caec298645793dea72c5d42aa3b782" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.repair-request.json", + "bytes": 2225, + "sha256": "f4f6b5480ec8927033ea469ea82c1e8411cb6dba95d2da680f15bc193914c9b4" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.steps.md", + "bytes": 3897, + "sha256": "7d7e38d93641a5f22a2738baa4dbacc392eebb477a855cdce0641961980a19ad" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.take-plan.json", + "bytes": 18557, + "sha256": "74dd68ba686b8928054480f0bfed55aad91f6f1208803399dc9878e894f57ecb" + }, + { + "path": "harness/fixtures/release1-native/semantic-keyed.take-request.json", + "bytes": 1508, + "sha256": "d19a53002a6bf95af3123a3ef9f78baaf7ce1085d4f06f0f4a9d5e5d5accbc0e" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.arcadia", + "bytes": 6935, + "sha256": "90277f1780d43a6f0b2184bf00b87e7a4f3518d67c6f10d4793daafe1aabf493" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.after-order.json", + "bytes": 11286, + "sha256": "5a3c8494f6976c23b990c14b40760fb2f32ca9973caf6f31dab5632310663591" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous-request.json", + "bytes": 1379, + "sha256": "6a86fde194faa55f2d1bdb982571e0e711e5401b53a960e6acb8cf3e17f506c8" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.arcadia", + "bytes": 6195, + "sha256": "e7c30c5a5c3ba7209c8e223f02da55821d7af10a2f09844a7065ae6cd8f22aa5" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-plan.json", + "bytes": 19156, + "sha256": "d1e5c2ed08e0aa6274a0421a39876d1ba99f4e441c7a0a68660a83b9e017da14" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.final-request.json", + "bytes": 2418, + "sha256": "433f723252598994dac7184da61f95344b528156cc5a0fba1a7864b2ec9f002b" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.json", + "bytes": 10441, + "sha256": "9e4e2ce1c7879935ad0860a3ae720506c929a8422b51491b719d88af3fd3ff2d" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-branch-steps.manifest.json", + "bytes": 1042, + "sha256": "87e8899c7d4e54faedeef32afb7592fb65278d1bd7390850ab198dcf62068329" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.arcadia", + "bytes": 6280, + "sha256": "6c77ed42bbf9f4ebe08bfa233197a4020e5587ce33747f7914430aa0c662d9a3" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.after-order.json", + "bytes": 10009, + "sha256": "1dd7ca2bcfc84ac23369c506302b39569d2ebc41e5a4edc99378a1fe01923b7e" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous-request.json", + "bytes": 1390, + "sha256": "e0ea7aeb25687ae63c65128f1aab78273f14030d179d3fbea63fe91ab5430136" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.arcadia", + "bytes": 5529, + "sha256": "563c36162050c8564ffa25a85c726462ecdaebdb87659b3f3610506bfd5581e6" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.final-plan.json", + "bytes": 18821, + "sha256": "f96c43fc58dbe48a044b628b31bb3f0b59171dad5b6982ec6c34a5597e474130" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.final-request.json", + "bytes": 2438, + "sha256": "d38fdb13612b07254edad80848f79c1e097d5f868c020feae9ba072aa697a50d" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.json", + "bytes": 9153, + "sha256": "2ed0daed118f2df13dcad738dab174ad34d44a15921cf8097415e707978b0968" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-fragment.manifest.json", + "bytes": 1058, + "sha256": "c79bd12a85d8d4f25cc3ee43581b3e5e74ae9ad7e4de0c72764975ba2b89c279" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.arcadia", + "bytes": 5026, + "sha256": "98020e048ac01f285c6ad8c10ce22bef05e193b5ba0f5558be1fb26a7bf82620" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.after-order.json", + "bytes": 7851, + "sha256": "ea3783992488bc6ee944eb91a26ccf3dbc05a3909528fc32ec8023a76765cee6" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous-request.json", + "bytes": 1379, + "sha256": "ea3cfd25f7a50481805956fb88ab851516dcd602dd6564502a0cc55f7de6a54f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.arcadia", + "bytes": 4295, + "sha256": "2b7c3694623bbea3e36dcc6f674d4adcd91e23c8e1005ded21e5a69648a048e5" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.final-plan.json", + "bytes": 17912, + "sha256": "95a23bd334cffcaef0c3b1012b7555efd2a077ef6f9140d58020910ef8a76723" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.final-request.json", + "bytes": 2081, + "sha256": "1e2433ed5821bf65443f56cb27381ee935db81b1955d3a539b7952898312a410" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.json", + "bytes": 7012, + "sha256": "0033f8780c2ba69779b811bca792340c25a092f887e5811c6839f0a691d744f2" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-reverse.manifest.json", + "bytes": 1046, + "sha256": "e3b4a60a56abee779ca8356605c968aa2d99256a3bd3c5601b14b5c5502a1a05" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.arcadia", + "bytes": 5467, + "sha256": "e265cab35097acc46f5123107c13bc35b9f71931ff650557fc1c85779141b327" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.after-order.json", + "bytes": 8482, + "sha256": "9b777828d3681138de6bbd08de8bf922bed92c27be60b103d8ff73ce7ddd0a48" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous-request.json", + "bytes": 1379, + "sha256": "7d124256730bf2feee772c7d0118b87eb288e8aaa9179d69ceb9259dbceb2c71" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.arcadia", + "bytes": 4727, + "sha256": "87a5a3665dab2dd6e595867dcebb1586e93dd067c85d6e24b1a4d3ff0ad6d4ba" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.final-plan.json", + "bytes": 18803, + "sha256": "1888ca51e0923866a9afea99cad069fe5d3f19f0a1b24328e20ac1b9f52d8bad" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.final-request.json", + "bytes": 2294, + "sha256": "5070a5b85feb1bc7b1fc648cd5d35ef26663492c8666dcd471950c06ec3a6052" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.json", + "bytes": 7641, + "sha256": "725b35e36640c81c70c7efea5c3304bf61b49ef9664194ca0d491782cc62191a" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered-scenario.manifest.json", + "bytes": 1038, + "sha256": "1b43abf8fed3061c65a7b7cb103d31ea7ece333a2da83c645f8d228448c4dbb2" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.after-order.arcadia", + "bytes": 5019, + "sha256": "65a6217f474b5085f37b511151185c2c08ff7a1998e924f1fe01f29c410f6ced" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.after-order.json", + "bytes": 7835, + "sha256": "8a53bb9b43eab5b6dca5e08deef20c14c96e134b9ee816a99b7960bb5255e008" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.ambiguous-request.json", + "bytes": 1379, + "sha256": "c704f99245f00d5c3be0562d91931b4e7f1fb3fb36f41991eb76656ffbd6129f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.ambiguous.txt", + "bytes": 94, + "sha256": "0f938b23fcfce776a61a57de5a2c82b4a0720b5e5af6780fdb96f5f92b3edd21" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.arcadia", + "bytes": 4279, + "sha256": "c59ec1233d5a9fbb8e386f6d9b13f398be21baf813ba215efb51bfc1bb98db36" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.final-plan.json", + "bytes": 17938, + "sha256": "30c5c3f614a74708763df612016483623792f4c65e2ed2fa662ac417cfb852eb" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.final-request.json", + "bytes": 2098, + "sha256": "3b6c35ef78133f0980cb6fb53fbc29c5d3c4deb5dc4be01c2c3384e221987b9b" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.json", + "bytes": 6996, + "sha256": "c159c60403abcf8fefab7f19cbad9989488e8ccc20b82c57597b22a0ae254802" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.manifest.json", + "bytes": 1038, + "sha256": "af8a314a9b1d85a8f98affa5a6649f43d252851158798e1ad687bb1b6966b14f" + }, + { + "path": "harness/fixtures/release1-native/semantic-ordered.steps.md", + "bytes": 4549, + "sha256": "6004894e9d9f478b7fe1c3c3e0706dcc546d5df8557cff91b4cf26f8a4badf24" + }, + { + "path": "harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json", + "bytes": 1933, + "sha256": "71cca2a59f36b2a07d11dedbe9101226aa1a4a00d8ac244b5565202e56709f51" + }, + { + "path": "harness/fixtures/release1-native/source5-recovery/generate.py", + "bytes": 1496, + "sha256": "e44986c1d563499f25917441220b23883fc9cb0f89faf409873191f0212fbd2f" + }, + { + "path": "harness/fixtures/release1-native/state-actions.arcadia", + "bytes": 6224, + "sha256": "9cde82608110e56aa9e14c9e33eb38976f53aed74ab013145d411d5743a0ec26" + }, + { + "path": "harness/fixtures/rest-coverage/README.md", + "bytes": 1870, + "sha256": "df4a391312e0c8b20ccdcc850199443b08547be1b10b1f2447a067bf4bf544e6" + }, + { + "path": "harness/fixtures/rest-coverage/coverage-probe.json", + "bytes": 7951, + "sha256": "2d1ef7bdb5d7bf17b9fda33b1d1790d1d88289b3fd1e725f89b83a69771f9a43" + }, + { + "path": "harness/fixtures/rest-coverage/run-coverage-probe.ps1", + "bytes": 9381, + "sha256": "f24f19fca418b76a7249894a08c116e16b465a02f8f899d12b808ccfdd90a3bd" + }, + { + "path": "harness/fixtures/run-timed-bundle-oracle.ps1", + "bytes": 7466, + "sha256": "fab696486a574f9867c6e69a8a8b960e7e9722206441b9cc5f36d41f285f1a25" + }, + { + "path": "harness/fixtures/scenario-authoring/README.md", + "bytes": 14700, + "sha256": "6b651b180b76d10b00bc49d77a8f5e0b3ba0317446978f41b541f5bb23fedad8" + }, + { + "path": "harness/fixtures/scenario-authoring/base/expected-baseline.json", + "bytes": 38589, + "sha256": "abd8246c939ebb8a2162ef82e23d0fb322ee1a8b4d0d7c28a34037e42e86e841" + }, + { + "path": "harness/fixtures/scenario-authoring/base/project.arcadia", + "bytes": 19470, + "sha256": "9b1afbe96b0bf8fa613451b00929492f05a59b9eb2ca74c2c374a8b7bb7cba9e" + }, + { + "path": "harness/fixtures/scenario-authoring/base/rationale.arcadia", + "bytes": 86, + "sha256": "82df210f22f6aa336a3e7e00240977d671c7c3923db7bc58fb29e6d98b67ef49" + }, + { + "path": "harness/fixtures/source7-native/README.md", + "bytes": 7235, + "sha256": "7abccbc3da0980d1d099761d1a66d12c403a0f543954134537ecd4930d182b4b" + }, + { + "path": "harness/fixtures/source7-native/base/expected-baseline.json", + "bytes": 26428, + "sha256": "e2882a58f841f460f45d9d67db1606440e0d7997b9b29a784569ae48e4b23bfb" + }, + { + "path": "harness/fixtures/source7-native/base/project.arcadia", + "bytes": 13616, + "sha256": "5474d8ca05fd1375220dc0ad40d913631fde51159e9632bb9f290d1ecdab8dd7" + }, + { + "path": "harness/fixtures/source7-native/base/rationale.arcadia", + "bytes": 59, + "sha256": "e6a600e13036d4ad594a65380ed1b3d82d1772d583041bb0a624c9588a73047a" + }, + { + "path": "harness/fixtures/source7-native/manifest.json", + "bytes": 410, + "sha256": "6c57164a796234d83818d462d0a25f10e77062d405769e602ae2a7bec251e3b6" + }, + { + "path": "harness/fixtures/staged-source-authoring/README.md", + "bytes": 6876, + "sha256": "502be5e3bf9112447dc07b1a51fe0fa301754b47128a9b81877c185c6e797fee" + }, + { + "path": "harness/fixtures/staged-source-authoring/base/notes.arcadia", + "bytes": 63, + "sha256": "ad9de02963c20d1f43856455a797deb05ca674659e45f2c4b96b52783024f8bd" + }, + { + "path": "harness/fixtures/staged-source-authoring/base/project.arcadia", + "bytes": 593, + "sha256": "aa844950ec4fb86f262a257e1fc370a26557ea7978d8a4cab78265037660234c" + }, + { + "path": "rust-toolchain.toml", + "bytes": 66, + "sha256": "a6a0bbd29ffaa8182dc22d1d9149709f1091e47df40ed96eb8a78a711c66a4ce" + } + ] +} diff --git a/harness/experiments/canvas-union-windows-20260911/initial-compile-shared-6969c82.log b/harness/experiments/canvas-union-windows-20260911/initial-compile-shared-6969c82.log new file mode 100644 index 00000000..9511672d --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/initial-compile-shared-6969c82.log @@ -0,0 +1 @@ +error: no library targets found in package `oa-desktop` diff --git a/harness/experiments/canvas-union-windows-20260911/initial-compile-shared-6969c82.result.json b/harness/experiments/canvas-union-windows-20260911/initial-compile-shared-6969c82.result.json new file mode 100644 index 00000000..c90ff2a4 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/initial-compile-shared-6969c82.result.json @@ -0,0 +1,7 @@ +{ + "source": "6969c825a8340980b6a419524ade3bb687a6051f", + "start": "2026-09-11T17:46:09.2871806Z", + "end": "2026-09-11T17:46:12.8288903Z", + "exit": 101, + "inputs": 281 +} diff --git a/harness/experiments/canvas-union-windows-20260911/manifest.json b/harness/experiments/canvas-union-windows-20260911/manifest.json new file mode 100644 index 00000000..530e249b --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/manifest.json @@ -0,0 +1,230 @@ +{ + "scope": "Closed Windows canvas union gates through abffa4b; native and post-cleanup acceptance not included. Test executables remain local; their fingerprints are metadata only.", + "files": [ + { + "path": "record.json", + "bytes": 3045, + "sha256": "6ae70f3067bfd32628dde484ac4fec62ed10fdd598b95ce78d4da6f331662563" + }, + { + "path": "candidate-commit.txt", + "bytes": 42, + "sha256": "0abdb11bc01a9588b073d13d8536f455a429f84e35c36fc872aad711ff5b736f" + }, + { + "path": "compile-inputs-6969c82.json", + "bytes": 48790, + "sha256": "02456a1c1260fd40ecbe8096ee6af3583749f6cd862586a4dd8044ed9ec1f797" + }, + { + "path": "compile-inputs-06ec962.json", + "bytes": 48790, + "sha256": "dd043656df69a8b25c5ba5bf5fc1aca137523d7920f048510e7cb3a061e3c806" + }, + { + "path": "full-inputs-ff01754.json", + "bytes": 96552, + "sha256": "966b125ec8862d0e48ed98692bc3dc380f0242007eb4c018621a9fde844562eb" + }, + { + "path": "guide-inputs-82fd0ac.json", + "bytes": 96552, + "sha256": "28e1a0c78997e507fe0435cffc926adfbd0c9ee3d9e9affffd38f061ec19b975" + }, + { + "path": "final-inputs-abffa4b.json", + "bytes": 96552, + "sha256": "1d4f5640e6b96ef1914fce416c2148e868f2e89d17edc890dec1f69f8812f635" + }, + { + "path": "tested-06ec962/manifest.json", + "bytes": 6028, + "sha256": "756c6049ebb6e595d30c384e0dbfa1a922a12dd93f38369723c883efbc582d87" + }, + { + "path": "tested-ff01754/artifact-manifest.json", + "bytes": 764, + "sha256": "cabd336b7f00e7412c10b8cba187f43b441d554b88c96453fb43bbc4cbd89631" + }, + { + "path": "initial-compile-shared-6969c82.log", + "bytes": 57, + "sha256": "97570773eda98d01ee18d3a67732336122f831acbe1c2429571201643a6a45cd" + }, + { + "path": "initial-compile-shared-6969c82.result.json", + "bytes": 182, + "sha256": "ee38e9b2768d6cd30e76e8f380c627c2196a1af1e2dc62d83b9678b6d9233b15" + }, + { + "path": "compile-shared-6969c82.log", + "bytes": 3378, + "sha256": "75e1c5506f1b128fdc19ab9b2e232d1b44eb1dbeedf01549ca6a617f9107d0ad" + }, + { + "path": "compile-shared-6969c82.result.json", + "bytes": 292, + "sha256": "7922c007d2efc44d048b06927f6382b1df69bdecb617ac6103c9f1d7cee4d9bd" + }, + { + "path": "compile-shared-06ec962.log", + "bytes": 1857, + "sha256": "3aeddd7b828ba9a73d98b4db2dd2fc3bc6909f1cde1887c378118004f9fe5f75" + }, + { + "path": "compile-shared-06ec962.result.json", + "bytes": 308, + "sha256": "6687346a818826bca3bb28942705092d6a51f645775d55b10d85a548bc792586" + }, + { + "path": "production17-06ec962.log", + "bytes": 4359, + "sha256": "977646806e102a1b1161faa61b2b1a5c44e23ef4cfb63414b166ce2116aee2b7" + }, + { + "path": "production17-06ec962.result.json", + "bytes": 278, + "sha256": "a6bef9638ac8393e802a15743e590e928f660c23c026277dd87f2be226086203" + }, + { + "path": "scenario21-06ec962.log", + "bytes": 4287, + "sha256": "0b58c048656616d49e6d3d4a340537570b54736664671f28103c3a7af7259255" + }, + { + "path": "scenario21-06ec962.result.json", + "bytes": 305, + "sha256": "bfb008a048cd8a540c54475112dca862495d30dbb11d621c925c9cd62ec10b24" + }, + { + "path": "full-ff01754.log", + "bytes": 83584, + "sha256": "a6d2e0d39b45246d88f1f5b61117cdb0897b23a0d072238eca7a5550495bd421" + }, + { + "path": "full-ff01754.result.json", + "bytes": 254, + "sha256": "c448190de3c3768e43c0e191274d81074927a74c91c6ee7e4dc9ad5369c2b92d" + }, + { + "path": "guide-82fd0ac.log", + "bytes": 641, + "sha256": "ad151c1ee3bac807f87d192a80015b7a1e659cd0bc7ee523ecff53a62c0e75a7" + }, + { + "path": "guide-82fd0ac.result.json", + "bytes": 267, + "sha256": "90203fdb4422730d946a7e5b2f21022586775e69de22a60791d0d25612d09bbd" + }, + { + "path": "clippy-82fd0ac.log", + "bytes": 1038, + "sha256": "941c49a48f1648000015bb2278cf490d66d00781774df7740c800f667271f210" + }, + { + "path": "clippy-82fd0ac.result.json", + "bytes": 246, + "sha256": "535e701e8e9d77c7d306c2985741e039b096eb6f5c9489f18b4c5ef6e7421835" + }, + { + "path": "clippy-abffa4b.log", + "bytes": 1786, + "sha256": "b2f21d94e7e6221210beec871eb74f2d255552ce6c3971df5125cf36cad07567" + }, + { + "path": "clippy-abffa4b.result.json", + "bytes": 246, + "sha256": "4f5339fcac106076619aff5676f981e454a43966c55d6e5f81c104996a5cd76e" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53d4a422d3066e07cccb9e8ef2ad08ea95e451b9b7cbef29a27e6634e035ed97" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "aa2e4853b25bb7e185b1de49eda42028523c8ebf9599bfe6b7cb2a2b2a688090" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7860, + "sha256": "851af1e9b155d6d0d079d9164c5616fadc33e961391a6604a43eabf5158957b3" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "cc58cb3838f546150d22fe4dd83a3ffd52504c279febaeb8d0b4a6a3b8a73950" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 27020, + "sha256": "d84d022574a6f1d69f203d9383502227fb9fc23605c6fb48779ca9f09bee546d" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "af1c7c1e802d1207ee6c7bd66b703811a250d7b56511512af6c0945c776e68a7" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7744, + "sha256": "096d667c4c8ee3732d52668a9562c665f2d9a192abda4e5487fcad7621c8dbfd" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950cf4347a3edea615b1ac71d9a681c32b446b5109fcfd0f03f3ae2b147360bf" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284f5a7da09bdc77d70c6788a091d57927150c796ffc10d786793cff7da4b7c6" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "fb1da03e8df05a59253f7fdf34391e30abd8aea427b7d0a12b86b80ff3bf9eca" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309b8978ec2586ff4614236c2f624af6d934d117363e917b8c1cf9f2b1295eb" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733f8badafb9ba87e3f861f03f78809450eee72cc118341eb03b24904790144a" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "a990947cb39b2fb15c21dfbc95ee16f92bf6be749d3872c6904762b9bde37089" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "b2d502c11426bffb8763515f2a56aa4c9f8e203bb36d65eb34f81c123fc406a6" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723e51a7da4eb224d51c1659ebbfa4dc8b11557bc8bf70f6e3e1649bad24449" + }, + { + "path": "tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5e7cecb54bfca64e4462f7881f460900abd925ba2cfe9451ce2b0c78af4bc850" + } + ] +} diff --git a/harness/experiments/canvas-union-windows-20260911/production17-06ec962.log b/harness/experiments/canvas-union-windows-20260911/production17-06ec962.log new file mode 100644 index 00000000..7914141c --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/production17-06ec962.log @@ -0,0 +1,55 @@ +warning: type `traceability::Event` is more private than the item `app::Message::Traceability::0` + --> crates\oa-desktop\src\app.rs:339:18 + | +339 | Traceability(crate::views::traceability::Event), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field `app::Message::Traceability::0` is reachable at visibility `pub` + | +note: but type `traceability::Event` is only usable at visibility `pub(crate)` + --> crates\oa-desktop\src\views\traceability.rs:12:1 + | + 12 | pub(crate) enum Event { + | ^^^^^^^^^^^^^^^^^^^^^ + = note: `#[warn(private_interfaces)]` on by default + +warning: type `traceability::State` is more private than the item `app::App::traceability` + --> crates\oa-desktop\src\app.rs:467:5 + | +467 | pub traceability: crate::views::traceability::State, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field `app::App::traceability` is reachable at visibility `pub` + | +note: but type `traceability::State` is only usable at visibility `pub(crate)` + --> crates\oa-desktop\src\views\traceability.rs:17:1 + | + 17 | pub(crate) struct State { + | ^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `oa-desktop` (bin "arcadia-studio" test) generated 2 warnings + Finished `test` profile [unoptimized] target(s) in 1.30s + Running unittests src\main.rs (target\debug\deps\arcadia_studio-e358a5ed59b1de41.exe) + +running 22 tests +test behavior::scenario::workspace::tests::production_empty_scenario_lifeline_tool_keeps_staged_source_inside_stable_canvas ... ok +test behavior::scenario::workspace::tests::production_scenario_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok +test behavior::scenario::workspace::tests::production_scenario_canvas_keeps_bounds_draft_identity_and_camera_through_editing ... ok +test behavior::scenario::workspace::tests::production_scenario_canvas_pan_invalidates_a_pressed_creation_and_caller_restores_once ... ok +test behavior::scenario::workspace::tests::production_scenario_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok +test behavior::scenario::workspace::tests::production_scenario_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok +test behavior::scenario::workspace::tests::production_scenario_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok +test behavior::scenario::workspace::tests::production_scenario_whole_glyph_drag_moves_one_occurrence_and_rejects_other_operand ... ok +test behavior::topology::direct_tests::direct_process_production_click_click_drag_insert_and_scroll_coordinates ... ok +test behavior::topology::direct_tests::direct_process_production_endpoint_and_peer_reorder_handles_publish_exact_identity ... ok +test behavior::topology::direct_tests::direct_process_production_stale_focus_scroll_bounds_and_pending_cancel ... ok +test behavior::topology::view::layout_tests::production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_review_owners ... ok +test behavior::topology::view::layout_tests::production_process_caller_return_rejects_its_previous_camera_lifetime ... ok +test behavior::topology::view::layout_tests::production_process_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok +test behavior::topology::view::layout_tests::production_process_canvas_focus_wheel_fit_and_stale_camera_are_presentation_only ... ok +test behavior::topology::view::layout_tests::production_process_canvas_scaled_path_creation_preserves_source_and_editor_bounds ... ok +test behavior::topology::view::layout_tests::production_process_form_replacement_resets_scroll_without_a_closed_render ... ok +test behavior::topology::view::layout_tests::production_process_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok +test behavior::topology::view::layout_tests::production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok +test behavior::topology::view::layout_tests::production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok +test hosted::client::tests::hosted_production_auth_proxy_prefix_and_located_rejection ... ok +test hosted::client::tests::hosted_production_roundtrip_preserves_comments_noops_and_conflicts ... ok + +test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 707 filtered out; finished in 21.47s + diff --git a/harness/experiments/canvas-union-windows-20260911/production17-06ec962.result.json b/harness/experiments/canvas-union-windows-20260911/production17-06ec962.result.json new file mode 100644 index 00000000..1b7c31f9 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/production17-06ec962.result.json @@ -0,0 +1,7 @@ +{ + "source": "06ec9621297627a1f7d5c3bc65efb237d1c9923e", + "start": "2026-09-11T17:53:07.9157207Z", + "end": "2026-09-11T17:53:31.1438144Z", + "exit": 0, + "command": "cargo test --locked -p oa-desktop --bin arcadia-studio production_ -- --test-threads=1 --nocapture" +} diff --git a/harness/experiments/canvas-union-windows-20260911/record.json b/harness/experiments/canvas-union-windows-20260911/record.json new file mode 100644 index 00000000..562286ee --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/record.json @@ -0,0 +1,83 @@ +{ + "root_before": "638d06c4e33a1f54203b234d0b49c3ecf551d27d", + "candidate_before": "e511c4153fb5bb484652434340e0e4c7000c93d0", + "common": "c0e66f94e3412b5c3495e96f7934ae94116505c9", + "raw_inputs": [ + [ + "crates/oa-desktop/src/app.rs", + "F059158240FBD41491C65D7BDDE318B0089A6DFED2688A741172535154078236" + ], + [ + "crates/oa-desktop/src/app/process_canvas.rs", + "8B80661A021E2F9F8E83BD9B0858D707F067C2E42CB180A3CDFA59A1467B3DF9" + ], + [ + "crates/oa-desktop/src/app/reference_authoring.rs", + "CFD96F4A38754C5FDA1B176B342162B7BFA7BFE0DCAE2A8309953A40395D92BF" + ], + [ + "crates/oa-desktop/src/behavior.rs", + "A35D03A1A63BB8FD6D2E5D394CC41AB93A6F306F05BDD89353C6B79503F92E64" + ], + [ + "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "096D667C4C8EE3732D52668A9562C665F2D9A192ABDA4E5487FCAD7621C8DBFD" + ], + [ + "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "AF1C7C1E802D1207EE6C7BD66B703811A250D7B56511512AF6C0945C776E68A7" + ], + [ + "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "851AF1E9B155D6D0D079D9164C5616FADC33E961391A6604A43EABF5158957B3" + ], + [ + "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "CC58CB3838F546150D22FE4DD83A3FFD52504C279FEBAEB8D0B4A6A3B8A73950" + ], + [ + "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "0272F78DAA97FAB75E971F1548A3E224C2F2D0A4D9D603DB90DBEB9F6D673C1E" + ], + [ + "crates/oa-desktop/src/behavior/form_session.rs", + "F5027FB3A5A5599A182E7D1A10DBA56DD66EB1F23A4BA4C4A1744C9A6A5B6E61" + ], + [ + "crates/oa-desktop/src/behavior/scenario.rs", + "59F859B246C3186AC5B156B46AEE3A0DDC38F79C1CF332C4FB03092214A43A93" + ], + [ + "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "FED77DDF304FD831CC163F6B8079BB6C17AC755BF141A5D30D5E64C04CBD9677" + ], + [ + "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "851772CE7C1DA0479DA7D6FB16EF17038653B43DB3B8EDF5E88605AAE4830C89" + ], + [ + "crates/oa-desktop/src/behavior/topology.rs", + "5723E51A7DA4EB224D51C1659EBBFA4DC8B11557BC8BF70F6E3E1649BAD24449" + ], + [ + "crates/oa-desktop/src/behavior/topology/view.rs", + "B2D502C11426BFFB8763515F2A56AA4C9F8E203BB36D65EB34F81C123FC406A6" + ], + [ + "crates/oa-desktop/src/behavior/topology/direct.rs", + "6102219487A0717C66190153C435B5B05226D156C59C7EAAB649249ADEF73644" + ], + [ + "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "D3CD44477BDEB31256BA52AA70383CEFC9805A8B172271D357884C95DDEF11D8" + ], + [ + "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "39D9C28201EC1B044EAE848B09AB276FF7FCF9F69FE6B5E39A6863F56A0111CE" + ], + [ + "crates/oa-desktop/src/behavior/topology/contextual.rs", + "5D0AC5B3E258FC70987A00EDE1E603681D99746596F2FCC46AD2B3667D65203A" + ] + ] +} diff --git a/harness/experiments/canvas-union-windows-20260911/scenario21-06ec962.log b/harness/experiments/canvas-union-windows-20260911/scenario21-06ec962.log new file mode 100644 index 00000000..9e1556a9 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/scenario21-06ec962.log @@ -0,0 +1,54 @@ +warning: type `traceability::Event` is more private than the item `app::Message::Traceability::0` + --> crates\oa-desktop\src\app.rs:339:18 + | +339 | Traceability(crate::views::traceability::Event), + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field `app::Message::Traceability::0` is reachable at visibility `pub` + | +note: but type `traceability::Event` is only usable at visibility `pub(crate)` + --> crates\oa-desktop\src\views\traceability.rs:12:1 + | + 12 | pub(crate) enum Event { + | ^^^^^^^^^^^^^^^^^^^^^ + = note: `#[warn(private_interfaces)]` on by default + +warning: type `traceability::State` is more private than the item `app::App::traceability` + --> crates\oa-desktop\src\app.rs:467:5 + | +467 | pub traceability: crate::views::traceability::State, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ field `app::App::traceability` is reachable at visibility `pub` + | +note: but type `traceability::State` is only usable at visibility `pub(crate)` + --> crates\oa-desktop\src\views\traceability.rs:17:1 + | + 17 | pub(crate) struct State { + | ^^^^^^^^^^^^^^^^^^^^^^^ + +warning: `oa-desktop` (bin "arcadia-studio" test) generated 2 warnings + Finished `test` profile [unoptimized] target(s) in 1.10s + Running unittests src\main.rs (target\debug\deps\arcadia_studio-e358a5ed59b1de41.exe) + +running 21 tests +test behavior::scenario::workspace::tests::new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session ... ok +test behavior::scenario::workspace::tests::production_empty_scenario_lifeline_tool_keeps_staged_source_inside_stable_canvas ... ok +test behavior::scenario::workspace::tests::production_scenario_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags ... ok +test behavior::scenario::workspace::tests::production_scenario_canvas_keeps_bounds_draft_identity_and_camera_through_editing ... ok +test behavior::scenario::workspace::tests::production_scenario_canvas_pan_invalidates_a_pressed_creation_and_caller_restores_once ... ok +test behavior::scenario::workspace::tests::production_scenario_keyboard_enters_activates_draws_focus_and_returns_to_inspector ... ok +test behavior::scenario::workspace::tests::production_scenario_keyboard_reveals_and_activates_initially_offscreen_last_tool ... ok +test behavior::scenario::workspace::tests::production_scenario_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes ... ok +test behavior::scenario::workspace::tests::production_scenario_whole_glyph_drag_moves_one_occurrence_and_rejects_other_operand ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_chained_apply_rechecks_live_review_owner ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_missing_exact_target_leaves_other_inputs_untouched ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_returns_from_back_use_and_escape_after_initial_focus_is_consumed ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_saved_action_is_inert_on_replacement_child_review_and_changed_context ... ok +test behavior::scenario::workspace::tests::scenario_form_focus_targets_the_name_and_rejects_stale_sessions_or_modals ... ok +test behavior::scenario::workspace::tests::scenario_workspace_choosing_tools_preserves_valid_and_invalid_pending_editors ... ok +test behavior::scenario::workspace::tests::scenario_workspace_fit_uses_measured_inner_viewport_and_rejects_nonfinite_inputs ... ok +test behavior::scenario::workspace::tests::scenario_workspace_open_and_back_restore_owner_and_mounted_camera_after_preselection ... ok +test behavior::scenario::workspace::tests::scenario_workspace_same_size_owner_and_document_changes_remeasure_fit_viewport ... ok +test behavior::scenario::workspace::tests::scenario_workspace_scaled_and_scrolled_scene_keeps_clicked_slots_and_message_endpoints ... ok +test behavior::scenario::workspace::tests::scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_identity ... ok +test behavior::scenario::workspace::tests::scenario_workspace_viewport_events_are_passive_guarded_and_do_not_apply_raw_forms ... ok + +test result: ok. 21 passed; 0 failed; 0 ignored; 0 measured; 708 filtered out; finished in 20.08s + diff --git a/harness/experiments/canvas-union-windows-20260911/scenario21-06ec962.result.json b/harness/experiments/canvas-union-windows-20260911/scenario21-06ec962.result.json new file mode 100644 index 00000000..40e7a4f0 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/scenario21-06ec962.result.json @@ -0,0 +1,7 @@ +{ + "source": "06ec9621297627a1f7d5c3bc65efb237d1c9923e", + "start": "2026-09-11T17:54:10.3071064Z", + "end": "2026-09-11T17:54:32.0128367Z", + "exit": 0, + "command": "cargo test --locked -p oa-desktop --bin arcadia-studio behavior::scenario::workspace::tests:: -- --test-threads=1 --nocapture" +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/manifest.json b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/manifest.json new file mode 100644 index 00000000..0490155d --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/manifest.json @@ -0,0 +1,141 @@ +{ + "source": "06ec9621297627a1f7d5c3bc65efb237d1c9923e", + "records": [ + { + "path": "crates/oa-desktop/src/app.rs", + "bytes": 220005, + "sha256": "aa2e4853b25bb7e185b1de49eda42028523c8ebf9599bfe6b7cb2a2b2a688090", + "git_sha256": "aa2e4853b25bb7e185b1de49eda42028523c8ebf9599bfe6b7cb2a2b2a688090", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/app/process_canvas.rs", + "bytes": 30660, + "sha256": "53d4a422d3066e07cccb9e8ef2ad08ea95e451b9b7cbef29a27e6634e035ed97", + "git_sha256": "53d4a422d3066e07cccb9e8ef2ad08ea95e451b9b7cbef29a27e6634e035ed97", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/app/reference_authoring.rs", + "bytes": 11250, + "sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77", + "git_sha256": "38e033e23e8135032f70117a376a5fe7bc68127c466559a35d7ec200932e3f77", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior.rs", + "bytes": 132668, + "sha256": "5e7cecb54bfca64e4462f7881f460900abd925ba2cfe9451ce2b0c78af4bc850", + "git_sha256": "5e7cecb54bfca64e4462f7881f460900abd925ba2cfe9451ce2b0c78af4bc850", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace.rs", + "bytes": 7744, + "sha256": "096d667c4c8ee3732d52668a9562c665f2d9a192abda4e5487fcad7621c8dbfd", + "git_sha256": "096d667c4c8ee3732d52668a9562c665f2d9a192abda4e5487fcad7621c8dbfd", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/control.rs", + "bytes": 7860, + "sha256": "851af1e9b155d6d0d079d9164c5616fadc33e961391a6604a43eabf5158957b3", + "git_sha256": "851af1e9b155d6d0d079d9164c5616fadc33e961391a6604a43eabf5158957b3", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/focus.rs", + "bytes": 8943, + "sha256": "cc58cb3838f546150d22fe4dd83a3ffd52504c279febaeb8d0b4a6a3b8a73950", + "git_sha256": "cc58cb3838f546150d22fe4dd83a3ffd52504c279febaeb8d0b4a6a3b8a73950", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/tests.rs", + "bytes": 27020, + "sha256": "d84d022574a6f1d69f203d9383502227fb9fc23605c6fb48779ca9f09bee546d", + "git_sha256": "d84d022574a6f1d69f203d9383502227fb9fc23605c6fb48779ca9f09bee546d", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs", + "bytes": 14436, + "sha256": "af1c7c1e802d1207ee6c7bd66b703811a250d7b56511512af6c0945c776e68a7", + "git_sha256": "af1c7c1e802d1207ee6c7bd66b703811a250d7b56511512af6c0945c776e68a7", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/scenario.rs", + "bytes": 152263, + "sha256": "fb1da03e8df05a59253f7fdf34391e30abd8aea427b7d0a12b86b80ff3bf9eca", + "git_sha256": "fb1da03e8df05a59253f7fdf34391e30abd8aea427b7d0a12b86b80ff3bf9eca", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace.rs", + "bytes": 26006, + "sha256": "284f5a7da09bdc77d70c6788a091d57927150c796ffc10d786793cff7da4b7c6", + "git_sha256": "284f5a7da09bdc77d70c6788a091d57927150c796ffc10d786793cff7da4b7c6", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/scenario/workspace/tests.rs", + "bytes": 79985, + "sha256": "950cf4347a3edea615b1ac71d9a681c32b446b5109fcfd0f03f3ae2b147360bf", + "git_sha256": "950cf4347a3edea615b1ac71d9a681c32b446b5109fcfd0f03f3ae2b147360bf", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/topology.rs", + "bytes": 84407, + "sha256": "5723e51a7da4eb224d51c1659ebbfa4dc8b11557bc8bf70f6e3e1649bad24449", + "git_sha256": "5723e51a7da4eb224d51c1659ebbfa4dc8b11557bc8bf70f6e3e1649bad24449", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/topology/contextual.rs", + "bytes": 28432, + "sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a", + "git_sha256": "5d0ac5b3e258fc70987a00ede1e603681d99746596f2fcc46ad2b3667d65203a", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct.rs", + "bytes": 36301, + "sha256": "9309b8978ec2586ff4614236c2f624af6d934d117363e917b8c1cf9f2b1295eb", + "git_sha256": "9309b8978ec2586ff4614236c2f624af6d934d117363e917b8c1cf9f2b1295eb", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/topology/direct_tests.rs", + "bytes": 50720, + "sha256": "733f8badafb9ba87e3f861f03f78809450eee72cc118341eb03b24904790144a", + "git_sha256": "733f8badafb9ba87e3f861f03f78809450eee72cc118341eb03b24904790144a", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/topology/layout_tests.rs", + "bytes": 36856, + "sha256": "a990947cb39b2fb15c21dfbc95ee16f92bf6be749d3872c6904762b9bde37089", + "git_sha256": "a990947cb39b2fb15c21dfbc95ee16f92bf6be749d3872c6904762b9bde37089", + "newline_only_difference": false + }, + { + "path": "crates/oa-desktop/src/behavior/topology/view.rs", + "bytes": 23136, + "sha256": "b2d502c11426bffb8763515f2a56aa4c9f8e203bb36d65eb34f81c123fc406a6", + "git_sha256": "b2d502c11426bffb8763515f2a56aa4c9f8e203bb36d65eb34f81c123fc406a6", + "newline_only_difference": false + }, + { + "path": "arcadia-studio-tests.exe", + "bytes": 139577856, + "sha256": "0d5009baa165bb7617ec90e2a387c474c872cc876ed4e80aaea9e5fa108ba304" + }, + { + "path": "libunwind.dll", + "bytes": 190464, + "sha256": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a" + } + ] +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app.rs new file mode 100644 index 00000000..13783226 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app.rs @@ -0,0 +1,5254 @@ +pub(crate) mod architecture_authoring; +#[cfg(test)] +mod architecture_authoring_tests; +mod checkpoints; +#[cfg(test)] +mod diagram_selection_tests; +mod hosted; +mod reference_authoring; +#[cfg(test)] +mod review_navigation_tests; +#[cfg(test)] +mod transition_context_tests; +#[cfg(test)] +mod view_copy_tests; +use crate::forms::Draft; +use crate::{ + behavior, + document::Document, + source_workspace::{self, LIMITS, Workspace}, +}; +use iced::{Element, Subscription, Task, keyboard, widget::text_editor, window}; +use oa_core::{projection::project_view, validation::validate, *}; +use oa_core::{ + source_files::{self, DiskProject}, + source_project::{CompiledSourceProject, SourceDiagnostic, SourceLocation}, +}; +use serde_json::{Value, json}; +use std::{collections::BTreeSet, path::PathBuf, sync::Arc}; + +#[cfg(test)] +mod direct_scenario_tests { + use super::*; + use crate::behavior::{ + self, + scenario::{self, CanvasEvent, Event, MessageStyle, Slot}, + }; + fn setup() -> (App, scenario::direct_tests::Fixture) { + let f = scenario::direct_tests::fixture(); + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(Page::Behavior)); + assert_eq!(app.scenario_context().unwrap().scenario, f.owner); + (app, f) + } + fn form(app: &mut App, event: Event) { + let _ = app.update(Message::Behavior(behavior::Event::Scenario(event))); + } + fn begin(app: &App, f: &scenario::direct_tests::Fixture) -> CanvasEvent { + CanvasEvent::Begin { + context: app.scenario_context().unwrap(), + slot: Slot { + parent: f.owner, + before: None, + }, + sender: f.sender, + receiver: f.receiver, + style: MessageStyle::Asynchronous, + } + } + #[test] + fn direct_scenario_scroll_and_real_resize_invalidate_only_pointer_context() { + let (mut app, f) = setup(); + let original = app.document.bundle().clone(); + let event = begin(&app, &f); + let context = app.scenario_context().unwrap(); + let size = app.window_size; + let _ = app.update(Message::WindowResized(size)); + assert_eq!(app.scenario_context(), Some(context)); + let _ = app.update(Message::WindowResized(iced::Size::new( + size.width + 20., + size.height, + ))); + assert_ne!(app.scenario_context(), Some(context)); + let _ = app.update(Message::ScenarioCanvas(event)); + assert!(!app.behavior.has_pending()); + let context = app.scenario_context().unwrap(); + for (area, offset) in [ + (scenario::ScrollArea::Canvas, iced::Vector::new(30., 150.)), + (scenario::ScrollArea::Page, iced::Vector::new(0., 90.)), + ] { + let before = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + context, area, offset, + ))); + assert_ne!(app.scenario_context(), Some(before)); + let current = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + context, area, offset, + ))); + assert_eq!( + app.scenario_context(), + Some(current), + "unchanged redraw notification retains context" + ); + } + let event = begin(&app, &f); + let _ = app.update(Message::ScenarioCanvas(event)); + form(&mut app, Event::Name("Keep my draft".into())); + let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + context, + scenario::ScrollArea::Page, + iced::Vector::new(0., 200.), + ))); + assert!(app.behavior.has_pending()); + assert_eq!(app.document.bundle(), &original); + let current = app.scenario_context().unwrap(); + for dimension in 0..3 { + let mut stale = current; + match dimension { + 0 => stale.document = Id::new_v4(), + 1 => stale.scenario = Id::new_v4(), + _ => stale.revision += 1, + } + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + stale, + scenario::ScrollArea::Canvas, + iced::Vector::new(0., 900.), + ))); + assert_eq!(app.scenario_context(), Some(current)); + } + assert!(!app.document.can_undo()); + } + #[test] + fn direct_scenario_rejects_every_stale_context_and_preserves_property_drafts() { + let (mut app, f) = setup(); + let before = app.document.snapshot(); + for dimension in 0..4 { + let mut event = begin(&app, &f); + let CanvasEvent::Begin { context, .. } = &mut event else { + unreachable!() + }; + match dimension { + 0 => context.document = Id::new_v4(), + 1 => context.scenario = Id::new_v4(), + 2 => context.revision += 1, + _ => context.epoch += 1, + } + let _ = app.update(Message::ScenarioCanvas(event)); + assert!(!app.behavior.has_pending()); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + } + let _ = app.update(Message::Field( + "/name".into(), + "Pending scenario name".into(), + )); + assert!(app.draft_dirty()); + let event = begin(&app, &f); + let _ = app.update(Message::ScenarioCanvas(event)); + assert!(!app.behavior.has_pending()); + assert!(app.draft_dirty()); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + } + #[test] + fn direct_scenario_creation_and_reorder_preserve_selection_and_each_undo_once() { + let (mut app, f) = setup(); + let original = app.document.bundle().clone(); + let event = begin(&app, &f); + let _ = app.update(Message::ScenarioCanvas(event)); + form( + &mut app, + Event::Exchange(Choice { + id: f.exchange, + label: "Contract".into(), + }), + ); + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending()); + let created = app.selected; + assert_ne!(created, f.owner); + assert_eq!( + app.project().elements[&f.owner].steps().unwrap(), + &[f.call, f.reply, created] + ); + let before_reorder = app.document.bundle().clone(); + let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Select( + context, f.sender, + ))); + assert_eq!( + app.scenario_context(), + Some(context), + "same-owner selection retains gesture context" + ); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Reorder { + context, + occurrence: created, + slot: Slot { + parent: f.owner, + before: Some(f.call), + }, + })); + assert_eq!(app.selected, created); + assert_eq!( + app.project().elements[&f.owner].steps().unwrap(), + &[created, f.call, f.reply] + ); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &before_reorder); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &original); + assert_eq!(app.project(), &f.project); + assert!(!app.document.can_undo()); + } + #[test] + fn direct_scenario_source_failure_and_navigation_keep_form_until_success() { + let (mut app, f) = setup(); + let bundle = app.document.bundle().clone(); + let mut limits = oa_core::source_project::ProjectLimits::default(); + limits.model.max_source_bytes = + bundle.files.values().map(String::len).sum::() + 4000; + app.document = Document::from_bundle(bundle.clone(), limits).unwrap(); + app.sync(); + let event = begin(&app, &f); + let _ = app.update(Message::ScenarioCanvas(event)); + form( + &mut app, + Event::Exchange(Choice { + id: f.exchange, + label: "Contract".into(), + }), + ); + form(&mut app, Event::Name("Long message".repeat(1024))); + let _ = app.update(Message::Page(Page::Catalog)); + assert_eq!(app.page, Page::Behavior); + form(&mut app, Event::Submit); + assert!(app.behavior.has_pending()); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo()); + form(&mut app, Event::Name("Recovered message".into())); + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending()); + assert_eq!(app.project().named(app.selected), "Recovered message"); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo()); + } +} + +#[cfg(test)] +mod behavior_lifecycle_tests; +mod process_canvas; +mod state_diagram; +#[cfg(test)] +mod state_diagram_tests; + +#[derive(Debug, Clone)] +pub enum LoadedSnapshot { + Source(DiskProject), + Legacy(PathBuf, Box, Arc<[u8]>), +} +#[derive(Debug, Clone)] +pub struct FileSnapshot { + pub document: Id, + pub revision: u64, + pub snapshot: Arc, + pub disk: Option, + pub path: PathBuf, +} + +#[derive(Debug, Clone)] +pub struct SaveFailure { + pub document: Id, + pub message: String, + pub recovery: Option, +} + +#[derive(Debug, Clone)] +pub struct RecoverySnapshot { + pub target: PathBuf, + /// Recovery already succeeded even when reloading its files fails. + pub disk: Result, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Page { + Home, + Diagram, + Catalog, + Behavior, + Data, + Traces, + Text, + Validation, + Guide, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Choice { + pub id: Id, + pub label: String, +} +impl std::fmt::Display for Choice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.label) + } +} +#[derive(Debug, Clone, Copy)] +pub enum Pending { + Recent, + Recover(Id), + HostedOpen, + New, + Demo, + Walkthrough, + Open, + Close(window::Id), +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FileOperation { + Open, + Save, + Export, +} +pub struct FilePrompt { + pub operation: FileOperation, + pub path: String, + pub overwrite: bool, +} + +#[derive(Debug, Clone)] +pub enum Message { + BehaviorControl(behavior::canvas_workspace::Intent), + Architecture(architecture_authoring::Event), + Home(crate::home::Event), + CheckpointTick, + CheckpointWritten(Id, Result<(), String>), + CheckpointLoaded(Result, String>), + Hosted(crate::hosted::Event), + PortAnchor(crate::port_edit::Event), + Connection(crate::connection_edit::Event), + Menu(crate::model_menu::Event), + CreateContextElement(Kind, bool), + SourceTree(crate::source_tree::Event), + SourceTreeCollapse, + SourceTreeLocate, + ExactStrings(bool), + Page(Page), + Guide(crate::guide::Event), + Traceability(crate::views::traceability::Event), + Behavior(crate::behavior::Event), + Layer(Layer), + Select(Id), + EditProperties(Id), + SelectNode(Id), + SelectProject, + Search(String), + StartTransition, + StartSemanticReview(Id), + Workflow(crate::workflow::Event), + CollapseExplorer(Id), + ExplorerGroup(crate::explorer::Group), + ExplorerCollapseAll, + ExplorerLocate, + Explore(Option), + BrowseGroup(crate::explorer::Group), + ExplorerPage(usize), + CreateKind(Kind), + Data(crate::data_view::Event), + Add(bool), + AddRelation, + PortAction(crate::port_workflow::Action), + AddRelationKind(RelationKind, bool), + InspectorTab(crate::navigation::InspectorTab), + RevealInDiagram(Id, Id), + AddDiagram, + DuplicateDiagram(Id), + AddArtifact, + AddTrace, + Field(String, String), + FieldText(String, text_editor::Action), + FieldShape(String, Value), + FieldAppend(String, Value), + FieldValuePath(String, crate::data_values::FieldPathKey), + FieldValueBrowse(String, Option, usize), + FieldReferenceBrowse(String, String, usize), + FieldReferenceOpen(crate::forms::reference_chooser::adapter::Open), + FieldReferenceEvent(crate::forms::reference_chooser::Event), + FieldVariant(String, Value), + FieldArray(String, Option, i32), + Apply, + RevertProperties, + ElementKind(Kind), + RelationKind(RelationKind), + Command(Command), + Delete, + ConfirmDelete, + CancelDelete, + Undo, + Redo, + Diagram(Choice), + Projection(ProjectionKind), + BehaviorOwner(Choice), + BehaviorDiagram(Choice), + StateDiagram(crate::behavior::state_edit::Event), + ScenarioCanvas(crate::behavior::scenario::CanvasEvent), + TopologyCanvas(crate::behavior::topology::CanvasEvent), + PlaceSelected, + HideSelected, + ToggleCollapse, + MoveNode(Id, Position), + Route(crate::route_edit::Event), + Zoom(f32), + Pan(iced::Vector), + Fit, + SourceEdit(text_editor::Action), + ApplySource, + RevertSource, + SourceUndo, + SourceRedo, + SourceFile(String), + SourceNewPath(String), + SourceCreateFile, + SourceCloseFile(String), + SourceLoadIncludes, + SourceRecover, + SourceRecovered(Result), + IncludesLoaded(u64, Result), + Lint(u64), + Linted(u64, Result, String>), + SourceDiagnostic(usize), + SourceRelated(SourceLocation), + SourceViewport, + SourceForSelection, + SourceNavigation(crate::source_navigation::Event), + Save(bool), + SavePath(Option), + Saved(Result, SaveFailure>), + Loaded(Result, String>), + Request(Pending), + Discard, + Cancel, + SaveAndContinue, + FilePath(String), + SubmitFile, + CancelFile, + Export, + Exported(Result), + WindowResized(iced::Size), + CanvasResized(iced::Size), + ShortcutUndo(bool), + Escape, +} + +pub struct App { + pub home: crate::home::State, + pub recovery: crate::recovery::State, + recent_target: Option, + checkpoint_close: Option, + pub hosted: crate::hosted::State, + pub connection: Option, + pub model_menu: Option, + reference_authoring: reference_authoring::State, + pub source_tree: crate::source_tree::State, + menu_sequence: u64, + pub canvas_document: Id, + pub canvas_epoch: u64, + /// Camera batches survive their own updates, never external presentation changes. + pub camera_epoch: u64, + camera_boundary_epoch: u64, + pub selected_bend: Option<(Id, usize)>, + pub exact_strings: bool, + pub document: Document, + pub page: Page, + /// Home resumes this page; explicit Architecture navigation remains separate. + last_workspace_page: Page, + pub guide: crate::guide::State, + pub traceability: crate::views::traceability::State, + pub behavior: crate::behavior::State, + pub data_state: crate::data_view::State, + pub inspector_tab: crate::navigation::InspectorTab, + pub layer: Layer, + pub selected: Id, + pub selected_node: Option, + pub draft: Option, + pub active_diagram: Option, + pub search: String, + pub create_kind: Kind, + pub architecture: Option, + pub architecture_palette_expanded: bool, + pub status: String, + pub busy: bool, + pub pending: Option, + pub delete_pending: bool, + pub path: Option, + /// A disk transaction requiring an explicit action, independent of lint. + pub pending_recovery: Option, + disk_project: Option, + pub file_prompt: Option, + continue_after_save: bool, + pub zoom: f32, + pub pan: iced::Vector, + pub window_size: iced::Size, + canvas_size: Option, + fit_to_view: bool, + pub source_editor: text_editor::Content, + pub source_workspace: Workspace, + pub source_snapshot: Arc, + pub source_diagnostics: Vec, + pub source_jump: Option, + source_select_line: bool, + pub source_navigation: crate::source_navigation::State, + pub model_issues: Vec, + pub migration: Option, + source_version: u64, + lint_running: Option, + lint_requested: Option, + pub explorer_collapsed: BTreeSet, + pub explorer_groups_collapsed: BTreeSet, + pub explorer_index: crate::explorer::Index, + pub explorer_focus: Option, + pub explorer_group: Option, + pub explorer_page: usize, + pub workflow: Option, +} + +impl App { + pub fn new() -> Self { + let mut app = Self::with_project(Project::empty()); + app.home = crate::home::State::load_default(); + app.recovery = crate::recovery::State::load_default(); + app.page = Page::Home; + app.status = "Create a model, open a project or explore the example.".into(); + app.fit(); + app + } + pub fn with_project(project: Project) -> Self { + let document = Document::from_project(project.clone(), LIMITS) + .expect("Application models are valid source projects"); + let source_workspace = Workspace::new(document.bundle(), true); + let source = source_workspace.text().to_owned(); + let model_issues = validate(&project); + let selected = project.id; + let populated = |layer| { + project.elements.values().any(|e| e.layer() == layer) + || project.diagrams.values().any(|d| d.layer == layer) + }; + let layer = if !populated(Layer::Oa) && populated(Layer::Sa) { + Layer::Sa + } else { + Layer::Oa + }; + let active_diagram = project + .diagrams + .values() + .filter(|d| d.layer == layer) + .min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id)) + .map(|d| d.id); + let draft = Draft::from_project(&project, selected); + let explorer_index = crate::explorer::Index::new(&project, layer); + let explorer_collapsed = project.elements.values().filter_map(|e| e.parent).collect(); + let mut app = Self { + home: Default::default(), + recovery: Default::default(), + recent_target: None, + checkpoint_close: None, + connection: None, + model_menu: None, + reference_authoring: Default::default(), + source_tree: Default::default(), + menu_sequence: 0, + canvas_document: Id::new_v4(), + canvas_epoch: 0, + camera_epoch: 0, + camera_boundary_epoch: 0, + selected_bend: None, + exact_strings: false, + document, + page: Page::Diagram, + last_workspace_page: Page::Diagram, + guide: crate::guide::State::default(), + traceability: Default::default(), + behavior: Default::default(), + data_state: Default::default(), + inspector_tab: Default::default(), + layer, + selected, + selected_node: None, + draft, + active_diagram, + search: String::new(), + architecture: None, + architecture_palette_expanded: false, + create_kind: if layer == Layer::Sa { + Kind::Function + } else { + Kind::Activity + }, + status: "Coastal rescue example · explore both engineering perspectives".into(), + hosted: Default::default(), + busy: false, + pending: None, + delete_pending: false, + path: None, + pending_recovery: None, + disk_project: None, + file_prompt: None, + continue_after_save: false, + zoom: 0.65, + pan: iced::Vector::new(24., 48.), + window_size: iced::Size::new(1440., 900.), + canvas_size: None, + fit_to_view: true, + source_editor: text_editor::Content::with_text(&source), + source_workspace, + source_snapshot: Arc::from(source), + source_diagnostics: vec![], + source_jump: None, + source_select_line: false, + source_navigation: crate::source_navigation::State::default(), + model_issues, + migration: None, + source_version: 0, + lint_running: None, + lint_requested: None, + explorer_collapsed, + explorer_groups_collapsed: BTreeSet::new(), + explorer_index, + explorer_focus: None, + explorer_group: None, + explorer_page: 0, + workflow: None, + }; + app.route_state_diagram_workspace(); + app.last_workspace_page = app.page; + app + } + pub fn project(&self) -> &Project { + self.document.project() + } + pub fn title(&self) -> String { + format!( + "{}{} — Arcadia Studio", + self.project() + .name + .split_whitespace() + .collect::>() + .join(" "), + if self.dirty() { " *" } else { "" } + ) + } + pub fn source_dirty(&self) -> bool { + self.source_workspace.dirty() + } + pub fn draft_dirty(&self) -> bool { + self.draft.as_ref().is_some_and(Draft::dirty) + } + pub fn dirty(&self) -> bool { + self.document.dirty() + || self.architecture.is_some() + || self.workflow.is_some() + || self.draft_dirty() + || self.source_dirty() + || self.behavior.has_pending() + } + fn sync_source(&mut self) { + self.source_workspace.accept(self.document.bundle()); + self.show_source_file(false); + self.source_diagnostics = self.document.compiled().diagnostics.clone(); + self.source_version += 1; + self.lint_requested = None; + } + fn show_source_file(&mut self, restore_cursor: bool) { + self.source_tree.reveal(&self.source_workspace.active); + self.source_select_line = false; + let source = self.source_workspace.text().to_owned(); + self.source_navigation.refresh(&source); + self.source_editor = text_editor::Content::with_text(&source); + self.source_snapshot = Arc::from(source); + self.source_jump = restore_cursor + .then(|| { + self.source_workspace + .files + .get(&self.source_workspace.active) + .map(|b| b.cursor) + }) + .flatten(); + } + fn selected_architecture_placement(&self) -> Option { + self.active_diagram + .and_then(|id| self.project().diagrams.get(&id)) + .and_then(|d| { + d.nodes + .values() + .find(|n| n.id == self.selected || n.element == self.selected) + }) + .map(|n| n.id) + } + fn refresh_draft(&mut self) { + self.selected_node = if self.page == Page::Behavior { + None + } else { + self.selected_architecture_placement() + }; + if let Some(context) = self.state_context() { + self.behavior.diagram.placement = self.project().diagrams[&context.diagram] + .nodes + .values() + .find(|n| n.id == self.selected || n.element == self.selected) + .map(|n| n.id); + } + self.draft = Draft::from_project(self.project(), self.selected); + if self.exact_strings + && let Some(d) = &mut self.draft + { + let _ = d.set_exact(true); + } + } + fn sync(&mut self) { + self.explorer_index = crate::explorer::Index::new(self.project(), self.layer); + self.model_issues = validate(self.project()); + self.normalize_creation(); + if !self.project().contains(self.selected) { + self.selected = self.project().id; + self.selected_node = None; + } + if let Some(focus) = self.explorer_focus { + self.explorer_group = self.explorer_index.group(focus); + if self.explorer_group.is_none() { + self.explorer_focus = None; + } + } + self.reveal_explorer_selection(); + if self.active_diagram.is_none_or(|id| { + self.project() + .diagrams + .get(&id) + .is_none_or(|d| d.layer != self.layer) + }) { + self.active_diagram = self + .project() + .diagrams + .values() + .filter(|d| d.layer == self.layer) + .min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id)) + .map(|d| d.id); + } + self.refresh_draft(); + self.sync_source(); + } + fn normalize_creation(&mut self) { + if !self.create_kind.supports(self.layer) { + self.create_kind = if self.layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + } + } + pub fn apply_source(&mut self) -> bool { + if self.behavior.has_pending() { + self.status = + "Apply or cancel the behavior form before applying source changes.".into(); + return false; + } + if !self.source_dirty() { + return true; + } + if self.draft_dirty() { + self.status = "Property and source drafts both exist. Revert one draft before applying the other; both are retained.".into(); + return false; + } + match source_workspace::compile(&self.source_workspace.bundle()) { + Ok(compiled) => { + match self.document.apply_compiled(compiled) { + Ok(_) => { + self.sync(); + self.status = if self.source_workspace.detached_dirty() { "Reachable files applied. Reattach or revert detached drafts before saving." } else { "All source files applied as one model change" }.into(); + true + } + Err(e) => { + self.status = e.to_string(); + false + } + } + } + Err(issues) => { + self.source_diagnostics = issues; + self.status = "Source contains errors. Your draft is preserved.".into(); + self.page = Page::Text; + false + } + } + } + pub fn apply(&mut self) -> bool { + if self.behavior.has_pending() { + self.status = "Apply or cancel the behavior form before changing the model.".into(); + return false; + } + if self.source_dirty() { + self.status = + "Apply all source drafts or revert them before changing the model.".into(); + return false; + } + if !self.draft_dirty() { + return true; + } + let original = self.selected; + let result = self + .draft + .as_ref() + .ok_or("No selection.".to_string()) + .and_then(|d| Ok((d.command()?, d.resulting_id()?))); + match result.and_then(|(command, id)| { + self.document + .execute(crate::port_edit::repair_owner_change( + self.document.project(), + command, + )) + .map(|_| id) + .map_err(|e| e.to_string()) + }) { + Ok(id) => { + if self.active_diagram == Some(original) { + self.active_diagram = Some(id); + } + if self.behavior.active_owner == Some(original) { + self.behavior.active_owner = Some(id); + } + if self.behavior.diagram.diagram == Some(original) { + self.behavior.diagram.diagram = Some(id); + } + if self.selected_node == Some(original) { + self.selected_node = Some(id); + } + if self.explorer_collapsed.remove(&original) { + self.explorer_collapsed.insert(id); + } + self.selected = id; + if let Some(layer) = self.project().layer_of(id) { + self.layer = layer; + } + if let Some((diagram, layer, node)) = self + .project() + .diagrams + .values() + .find(|d| d.id == id || d.nodes.contains_key(&id)) + .map(|d| (d.id, d.layer, d.nodes.contains_key(&id).then_some(id))) + { + self.layer = layer; + if self.page == Page::Behavior + && let Some(machine) = + self.project().diagrams[&diagram].projection.state_machine() + { + self.behavior.active_owner = Some(machine); + self.behavior.diagram.diagram = Some(diagram); + self.behavior.diagram.placement = node; + } else { + self.active_diagram = Some(diagram); + self.selected_node = node; + } + } + self.sync(); + self.status = "Properties applied".into(); + true + } + Err(e) => { + self.status = e; + false + } + } + } + fn execute(&mut self, command: Command) -> bool { + let command = crate::port_edit::repair_owner_change(self.project(), command); + let created = self.behavior.creation.created_by(self.project(), &command); + let scenario_selection = self + .behavior + .scenario + .created_selection(self.project(), &command) + .or_else(|| { + self.behavior + .topology + .created_selection(self.project(), &command) + }); + match self.document.execute(command) { + Ok(selected) => { + if let Some(created) = created { + self.layer = created.layer; + self.selected = created.behavior; + self.selected_node = None; + self.behavior.active_owner = Some(created.behavior); + self.behavior.diagram = behavior::state_view::State::default(); + self.behavior.diagram.diagram = created.diagram; + self.page = Page::Behavior; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } else if let Some(id) = scenario_selection.or(selected) { + self.selected = id; + } + self.sync(); + self.status = "Model updated".into(); + true + } + Err(e) => { + self.status = e.to_string(); + false + } + } + } + fn apply_behavior(&mut self) -> bool { + if !self.behavior.has_pending() { + return true; + } + if self.source_dirty() || self.draft_dirty() { + self.status = "Property, source and behavior drafts are retained. Revert the other drafts before applying the behavior form.".into(); + return false; + } + match self.behavior.prepare_pending(self.project()) { + Ok(Some(command)) => { + if self.execute(command) { + self.behavior.acknowledge_commit(); + true + } else { + false + } + } + Ok(None) => true, + Err(error) => { + self.status = error; + false + } + } + } + fn reset(&mut self, project: Project, path: Option) { + self.retire_checkpoint(); + let home = std::mem::take(&mut self.home); + let recovery = std::mem::take(&mut self.recovery); + let window_size = self.window_size; + let canvas_size = self.canvas_size; + let source_version = self.source_version + 1; + let lint_running = self.lint_running; + let guide = std::mem::take(&mut self.guide); + let mut hosted = std::mem::take(&mut self.hosted); + hosted.leave_document(); + *self = Self::with_project(project); + self.home = home; + self.recovery = recovery; + self.hosted = hosted; + self.window_size = window_size; + self.canvas_size = canvas_size; + self.source_version = source_version; + self.lint_running = lint_running; + self.guide = guide; + self.path = path; + self.status = "Project ready".into(); + self.fit(); + } + fn diagram_for(&self, layer: Layer, element: Id) -> Option { + preferred_diagram(self.project(), self.active_diagram, layer, element) + } + /// Navigation after an accepted review is not another model operation. + /// Callers retain their workspace policy; keep an already visible target's + /// camera and never leave an old-layer diagram behind after counterpart focus. + fn focus_after_review(&mut self, target: Id, destination: Option) -> bool { + if !self.project().contains(target) { + return false; + } + let previous = (self.layer, self.active_diagram); + self.selected = target; + self.selected_node = None; + if let Some(layer) = self.project().layer_of(target) { + let retained = self.active_diagram.filter(|id| { + self.project().diagrams.get(id).is_some_and(|d| { + d.layer == layer + && (d.nodes.values().any(|n| n.element == target) + || self.project().elements.get(&target).is_some_and(|e| { + e.kind() == Kind::Port + && project_view(self.project(), *id) + .ports + .iter() + .any(|port| port.element == target) + })) + }) + }); + self.layer = layer; + self.active_diagram = destination + .filter(|id| { + self.project() + .diagrams + .get(id) + .is_some_and(|d| d.layer == layer) + }) + .or(retained) + .or_else(|| self.diagram_for(layer, target)); + } + self.refresh_draft(); + previous != (self.layer, self.active_diagram) + } + fn select(&mut self, id: Id) { + if self.selected == id { + return; + } + if !self.apply() { + return; + } + if !self.project().contains(id) { + return; + } + let previous_state_view = self.state_view_identity(); + self.selected = id; + self.selected_node = None; + if let Some(layer) = self.project().layer_of(id) + && layer != self.layer + { + self.layer = layer; + self.active_diagram = self.diagram_for(layer, id); + self.fit(); + } + if self.page == Page::Behavior + && let Some(owner) = crate::behavior::selected_behavior(self.project(), id) + { + self.behavior.active_owner = Some(owner); + } + if self.reset_state_view_after_navigation(previous_state_view) + && self.page == Page::Behavior + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + self.refresh_draft(); + } + fn reveal_explorer_selection(&mut self) { + if let Some(group) = self.explorer_index.group(self.selected) { + self.explorer_groups_collapsed.remove(&group); + for id in self.explorer_index.ancestry(self.selected) { + if id != self.selected { + self.explorer_collapsed.remove(&id); + } + } + } + if self + .explorer_focus + .is_some_and(|id| !self.project().elements.contains_key(&id)) + { + self.explorer_focus = None; + } + } + pub fn view(&self) -> Element<'_, Message> { + let base = crate::context_menu::Popup::new( + crate::views::view(self), + crate::model_menu::popup(self), + ) + .into(); + crate::behavior::scenario::focus::root( + behavior::canvas_workspace::focus_root( + crate::hosted::view::overlay(self, self.reference_overlay(base)), + behavior::canvas_workspace::Scope::input_owner(self), + ), + self.scenario_focus_owner(), + ) + } + pub fn subscription(&self) -> Subscription { + Subscription::batch([ + iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick), + window::close_requests().map(|id| Message::Request(Pending::Close(id))), + window::resize_events().map(|(_, size)| Message::WindowResized(size)), + architecture_authoring::subscription(self), + behavior::creation::subscription( + self.behavior.creation.has_pending() || self.behavior.source_active(), + ) + .map(|()| Message::Escape), + iced::event::listen_with(|event, status, _| { + let iced::Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers: mods, + .. + }) = event + else { + return None; + }; + if key == keyboard::Key::Named(keyboard::key::Named::Escape) { + if status == iced::event::Status::Captured { + return None; + } + return Some(Message::Escape); + } + if key == keyboard::Key::Named(keyboard::key::Named::F1) { + return Some(Message::Page(Page::Guide)); + } + if key == keyboard::Key::Named(keyboard::key::Named::F3) { + return Some(Message::SourceNavigation( + crate::source_navigation::Event::Next(mods.shift()), + )); + } + if !mods.command() { + return None; + } + match key.as_ref() { + keyboard::Key::Character("s") => Some(Message::Save(mods.shift())), + keyboard::Key::Character("o") => Some(Message::Request(Pending::Open)), + keyboard::Key::Character("n") => Some(Message::Request(Pending::New)), + keyboard::Key::Character("z") => Some(Message::ShortcutUndo(mods.shift())), + keyboard::Key::Character("y") => Some(Message::ShortcutUndo(true)), + keyboard::Key::Character("f") => Some(Message::SourceNavigation( + crate::source_navigation::Event::Open(false), + )), + keyboard::Key::Character("g") => Some(Message::SourceNavigation( + crate::source_navigation::Event::Open(true), + )), + _ => None, + } + }), + ]) + } + pub fn update(&mut self, message: Message) -> Task { + if let Message::BehaviorControl(intent) = message { + return self.behavior_control(intent); + } + let camera = matches!( + &message, + Message::ScenarioCanvas(crate::behavior::scenario::CanvasEvent::Camera(..)) + | Message::TopologyCanvas(crate::behavior::topology::CanvasEvent::Camera(..)) + ); + let before = self.camera_boundary(); + let task = self.update_inner(message); + if !camera && self.camera_boundary() != before { + self.camera_epoch = self.camera_epoch.wrapping_add(1); + self.camera_boundary_epoch = self.canvas_epoch; + } + task + } + fn behavior_control(&mut self, intent: behavior::canvas_workspace::Intent) -> Task { + use behavior::canvas_workspace::{self, Event as Camera, Scope}; + use behavior::scenario::CanvasEvent as Scenario; + use behavior::topology::CanvasEvent as Process; + if Scope::input_owner(self) != Some(intent.scope) { + return Task::none(); + } + let mut message = *intent.message; + let scope = intent.scope; + let valid_scenario = |c: behavior::scenario::Context| { + c.document == scope.document + && c.scenario == scope.owner + && c.revision == scope.revision + && self.camera_toolbar_epoch(c.epoch) + }; + let valid_process = |c: behavior::topology::Context| { + c.document == scope.document + && c.owner == scope.owner + && c.revision == scope.revision + && self.camera_toolbar_epoch(c.epoch) + }; + // This whitelist contains only the production presentation controls. + // A stamped toolbar cannot smuggle an arbitrary authoring command. + match &mut message { + Message::ScenarioCanvas(Scenario::Camera(c, lifetime, Camera::Focus)) + if valid_scenario(*c) && *lifetime == scope.epoch => + { + return canvas_workspace::focus_task(scope, Camera::Focus); + } + Message::TopologyCanvas(Process::Camera(c, lifetime, Camera::Focus)) + if valid_process(*c) && *lifetime == scope.epoch => + { + return canvas_workspace::focus_task(scope, Camera::Focus); + } + Message::ScenarioCanvas( + Scenario::Zoom(c, _) + | Scenario::Choose(c, _) + | Scenario::Focus(c) + | Scenario::Shelf(c, _), + ) if valid_scenario(*c) => { + c.epoch = self.canvas_epoch; + } + Message::TopologyCanvas( + Process::Zoom(c, _) | Process::Choose(c, _) | Process::Focus(c), + ) if valid_process(*c) => { + c.epoch = self.canvas_epoch; + } + Message::Behavior(behavior::Event::Topology(behavior::topology::Event::Shelf(_))) + if self.topology_context().is_some() => {} + _ => return Task::none(), + } + let task = self.update(message); + // Reapplying Fit or a tool also ends a held gesture. Focus is stamped + // after the action, for Iced's final-view ordering. + if self.camera_epoch == scope.epoch { + self.camera_epoch = self.camera_epoch.wrapping_add(1); + self.camera_boundary_epoch = self.canvas_epoch; + } + let focus = Scope::input_owner(self).map_or_else(Task::none, |scope| { + canvas_workspace::focus_task(scope, Camera::ControlFocus(intent.name)) + }); + Task::batch([task, focus]) + } + fn camera_toolbar_epoch(&self, epoch: u64) -> bool { + epoch >= self.camera_boundary_epoch && epoch <= self.canvas_epoch + } + fn camera_boundary(&self) -> impl PartialEq + use<> { + ( + self.canvas_epoch, + self.canvas_document, + self.document.revision(), + crate::behavior::selected_owner(self), + self.page, + self.window_size, + crate::behavior::canvas_workspace::keyboard_enabled(self), + self.behavior.has_pending(), + self.draft_dirty(), + self.source_dirty(), + self.behavior.topology.camera_boundary(), + self.behavior.scenario.camera_boundary(), + ) + } + fn update_inner(&mut self, message: Message) -> Task { + if let Some(task) = self.reference_message(&message) { + return task; + } + if matches!(&message, Message::Connection(_)) + && self + .architecture + .as_ref() + .is_some_and(architecture_authoring::State::unnamed_tool) + { + self.architecture = None; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + // Named placement owns input. Passive geometry/checkpoints continue; + // navigation and close retain raw input until explicit completion/cancel. + if self.architecture.is_some() { + if matches!(message, Message::Escape) { + return self.architecture_event(architecture_authoring::Event::Cancel); + } + let placing = self.architecture.as_ref().is_some_and(|s| s.saved.placing); + if !matches!( + &message, + Message::Architecture(_) + | Message::CheckpointTick + | Message::CheckpointWritten(..) + | Message::Lint(_) + | Message::Linted(..) + | Message::WindowResized(_) + | Message::CanvasResized(_) + ) && !(placing && matches!(&message, Message::Pan(_) | Message::Zoom(_))) + { + self.status = "Finish placing this element or Cancel (Esc) before leaving. Your name is retained.".into(); + return Task::none(); + } + } + // Scrollable geometry can change while async work or an overlay blocks + // commands. Keep the observed translation current; its passive handler + // still validates document, scenario and revision before accepting it. + if matches!( + &message, + Message::ScenarioCanvas( + crate::behavior::scenario::CanvasEvent::Scrolled(..) + | crate::behavior::scenario::CanvasEvent::Restored(..) + | crate::behavior::scenario::CanvasEvent::Viewport(..) + ) | Message::TopologyCanvas( + crate::behavior::topology::CanvasEvent::Scrolled(..) + | crate::behavior::topology::CanvasEvent::Viewport(..) + ) + ) { + return self.handle(message); + } + // Global subscriptions also receive keys consumed by an overlay. + if self.model_menu.is_some() && matches!(message, Message::Escape) { + self.model_menu = None; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + return Task::none(); + } + if self.model_menu.is_some() + && !matches!( + message, + Message::Menu(_) + | Message::CheckpointWritten(..) + | Message::CheckpointLoaded(..) + | Message::CheckpointTick + | Message::WindowResized(_) + | Message::CanvasResized(_) + | Message::Lint(_) + | Message::Linted(_, _) + | Message::SourceViewport + | Message::Request(Pending::Close(_)) + ) + { + return Task::none(); + } + if matches!( + message, + Message::WindowResized(_) | Message::Request(Pending::Close(_)) + ) { + self.model_menu = None; + } + if (self.source_dirty() || self.behavior.has_pending()) + && matches!( + message, + Message::Field(..) + | Message::FieldText(..) + | Message::FieldShape(..) + | Message::FieldAppend(..) + | Message::FieldValuePath(..) + | Message::FieldVariant(..) + | Message::FieldArray(..) + | Message::ElementKind(_) + | Message::RelationKind(_) + ) + { + self.status = + "Apply or cancel pending source and behavior drafts before editing properties." + .into(); + return Task::none(); + } + if self.busy + && !matches!( + message, + Message::CheckpointWritten(..) + | Message::CheckpointLoaded(..) + | Message::Saved(_) + | Message::Hosted(crate::hosted::Event::Completed(..)) + | Message::Loaded(_) + | Message::IncludesLoaded(_, _) + | Message::SavePath(_) + | Message::SourceRecovered(_) + | Message::Linted(_, _) + | Message::Exported(_) + | Message::WindowResized(_) + | Message::CanvasResized(_) + | Message::SourceViewport + ) + { + return Task::none(); + } + // A modal owns input until it is dismissed. This also protects against + // global shortcuts and controls that were focused before it opened. + if matches!( + message, + Message::Escape | Message::Request(Pending::Close(_)) + ) { + if self.hosted.visible { + self.hosted.visible = false; + return Task::none(); + } + if self.workflow.take().is_some() { + return Task::none(); + } + if self.file_prompt.take().is_some() { + self.continue_after_save = false; + return Task::none(); + } + if self.delete_pending { + self.delete_pending = false; + return Task::none(); + } + if self.pending.take().is_some() { + self.continue_after_save = false; + return Task::none(); + } + } + let modal_event = matches!( + message, + Message::CheckpointWritten(..) + | Message::CheckpointTick + | Message::CheckpointLoaded(..) + | Message::WindowResized(_) + | Message::CanvasResized(_) + | Message::SourceViewport + | Message::Lint(_) + | Message::Linted(_, _) + | Message::Saved(_) + | Message::Hosted(crate::hosted::Event::Completed(..)) + | Message::Loaded(_) + | Message::IncludesLoaded(_, _) + | Message::SavePath(_) + | Message::SourceRecovered(_) + | Message::Exported(_) + ); + if !modal_event + && ((self.hosted.visible && !matches!(message, Message::Hosted(_))) + || (self.workflow.is_some() && !matches!(message, Message::Workflow(_))) + || (self.file_prompt.is_some() + && !matches!( + message, + Message::FilePath(_) | Message::SubmitFile | Message::CancelFile + )) + || (self.delete_pending + && !matches!(message, Message::ConfirmDelete | Message::CancelDelete)) + || (self.pending.is_some() + && self.file_prompt.is_none() + && !matches!( + message, + Message::Cancel | Message::Discard | Message::SaveAndContinue + ))) + { + return Task::none(); + } + if matches!( + message, + Message::Page(_) | Message::Layer(_) | Message::Diagram(_) + ) { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.selected_bend = None; + } + if self.behavior.has_pending() + && matches!( + message, + Message::Page(page) if page != Page::Behavior + ) + { + self.status = "Apply or cancel the behavior form before leaving this view.".into(); + return Task::none(); + } + if self.behavior.has_pending() + && matches!( + message, + Message::BrowseGroup(_) + | Message::SourceEdit(_) + | Message::SourceCreateFile + | Message::SourceCloseFile(_) + | Message::SourceUndo + | Message::SourceRedo + | Message::SourceRecover + | Message::SourceLoadIncludes + | Message::Menu(_) + | Message::Connection(_) + | Message::PortAnchor(_) + | Message::Route(_) + ) + { + self.status = "Apply or cancel the behavior form before changing another view.".into(); + return Task::none(); + } + let previous = self.selected; + let previous_layer = self.layer; + let entering_catalog = + self.page != Page::Catalog && matches!(message, Message::Page(Page::Catalog)); + if self.page != Page::Home { + self.last_workspace_page = self.page; + } + let mut task = self.handle(message); + self.reconcile_reference_chooser(); + self.route_state_diagram_workspace(); + if self.page != Page::Home { + self.last_workspace_page = self.page; + } + if let Some(context) = self.topology_context() { + self.behavior + .topology + .enter(context.document, context.owner); + } + if let Some(context) = self.scenario_context() { + self.behavior + .scenario + .enter(context.document, context.scenario); + } + if self.connection.as_ref().is_some_and(|session| { + Some(session.context) != self.route_context() + || self.page != Page::Diagram + || self.draft_dirty() + || self.source_dirty() + }) { + self.connection = None; + } + if previous_layer != self.layer { + self.data_state = Default::default(); + self.explorer_index = crate::explorer::Index::new(self.project(), self.layer); + self.explorer_focus = None; + self.explorer_group = None; + self.explorer_page = 0; + task = task.chain(Task::batch(["model-explorer", "model-browser"].map(|id| { + iced::widget::operation::snap_to( + id, + iced::widget::scrollable::RelativeOffset::START, + ) + }))); + } + self.normalize_creation(); + if self.page == Page::Catalog && (self.selected != previous || entering_catalog) { + self.explorer_focus = self + .project() + .elements + .get(&self.selected) + .filter(|element| element.layer() == self.layer) + .map(|element| element.id); + self.explorer_group = self.explorer_index.group(self.selected); + self.explorer_page = 0; + } + if self.selected != previous { + self.reveal_explorer_selection(); + task.chain(iced::widget::operation::snap_to( + "inspector", + iced::widget::scrollable::RelativeOffset::START, + )) + } else { + task + } + } + pub(crate) fn scenario_context(&self) -> Option { + let scenario = crate::behavior::selected_owner(self)?; + (self.page == Page::Behavior + && self.project().elements.get(&scenario)?.kind() == Kind::Scenario) + .then_some(crate::behavior::scenario::Context { + document: self.canvas_document, + scenario, + revision: self.document.revision(), + epoch: self.canvas_epoch, + }) + } + pub(crate) fn scenario_focus_owner(&self) -> Option { + if self.busy + || self.hosted.visible + || self.workflow.is_some() + || self.architecture.is_some() + || self.model_menu.is_some() + || self.file_prompt.is_some() + || self.delete_pending + || self.pending.is_some() + { + return None; + } + self.behavior.scenario.focus_stamp(self.scenario_context()?) + } + fn handle(&mut self, message: Message) -> Task { + match message { + Message::BehaviorControl(intent) => return self.behavior_control(intent), + Message::Architecture(event) => return self.architecture_event(event), + Message::Home(event) => return self.home_event(event), + Message::CheckpointTick => return self.checkpoint_tick(), + Message::CheckpointWritten(id, result) => { + self.checkpoint_written(id, result); + if let Some(window) = self.checkpoint_close.take() { + self.retire_checkpoint(); + return window::close(window); + } + } + Message::CheckpointLoaded(result) => return self.checkpoint_loaded(result), + Message::Hosted(event) => return self.hosted_event(event), + Message::ScenarioCanvas(event) => { + use crate::behavior::scenario::{self, CanvasEvent}; + let context = event.context(); + if let CanvasEvent::FormShown(_, mount) = event { + if self.scenario_context().is_some_and(|current| { + current.document == context.document + && current.scenario == context.scenario + && current.revision == context.revision + }) && self.scenario_focus_owner().is_some() + && self.behavior.scenario.take_form_focus(mount) + { + return scenario::focus::task(self.scenario_focus_owner().unwrap()); + } + return Task::none(); + } + if let CanvasEvent::Viewport(_, size) = event { + if self.scenario_context().is_some_and(|current| { + current.document == context.document + && current.scenario == context.scenario + && current.revision == context.revision + }) && self.behavior.scenario.viewport(size) + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + return Task::none(); + } + if let CanvasEvent::Restored(_, token, area, offset) = event { + if self.scenario_context().is_some_and(|current| { + current.document == context.document && current.scenario == context.scenario + }) && self + .behavior + .scenario + .restored(context, token, area, offset) + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + return Task::none(); + } + if let CanvasEvent::Scrolled(_, area, offset) = event { + // Canvas and outline notifications may come from the + // same frame. Accept both across the first epoch bump, + // while still rejecting another owner/document/revision. + if self.scenario_context().is_some_and(|current| { + current.document == context.document + && current.scenario == context.scenario + && current.revision == context.revision + }) && self.behavior.scenario.scrolled(area, offset) + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + return Task::none(); + } + if let CanvasEvent::Camera(_, lifetime, input) = event { + use crate::behavior::canvas_workspace::{self, Event}; + if lifetime != self.camera_epoch + || !self.scenario_context().is_some_and(|c| { + c.document == context.document + && c.scenario == context.scenario + && c.revision == context.revision + }) + { + return Task::none(); + } + let scope = canvas_workspace::Scope { + document: context.document, + owner: context.scenario, + revision: context.revision, + epoch: lifetime, + }; + if self.behavior.has_pending() + || self.draft_dirty() + || self.source_dirty() + || !canvas_workspace::keyboard_enabled(self) + { + return Task::none(); + } + match input { + Event::Focus + | Event::Traverse(_) + | Event::ControlFocus(_) + | Event::ToggleFocus + | Event::Leave => { + return canvas_workspace::focus_task(scope, input); + } + Event::CancelGesture => { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1) + } + input => { + if self.behavior.scenario.camera(input) { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + } + return Task::none(); + } + if self.scenario_context() != Some(context) { + return Task::none(); + } + match event { + CanvasEvent::Focus(_) => { + self.behavior.scenario.toggle_focus(); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + CanvasEvent::Shelf(_, shelf) => { + self.behavior.scenario.toggle_shelf(shelf); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + CanvasEvent::Zoom(_, scale) => { + let mut state = std::mem::take(&mut self.behavior.scenario); + if state.camera_zoom(self.project(), context.scenario, scale) { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + self.behavior.scenario = state; + } + CanvasEvent::OpenUse(_, occurrence) => { + if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() + { + self.status = "Apply or cancel pending edits before opening a referenced scenario.".into(); + return Task::none(); + } + let mut state = std::mem::take(&mut self.behavior.scenario); + let target = state.open_use(self.project(), context, occurrence); + self.behavior.scenario = state; + match target { + Ok(target) => { + // A command can preselect this target while the + // caller remains visible, bypassing select below. + self.behavior.active_owner = Some(target); + self.select(target); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + Err(error) => self.status = error, + } + } + CanvasEvent::Back(_) => { + if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() + { + self.status = + "Apply or cancel pending edits before returning to the caller." + .into(); + return Task::none(); + } + let mut state = std::mem::take(&mut self.behavior.scenario); + let caller = state.back(self.project(), context); + self.behavior.scenario = state; + if let Some(caller) = caller { + // A command may already have selected this UUID while + // leaving the referenced definition open. select's + // same-UUID fast path must not keep that old owner. + self.behavior.active_owner = Some(caller.owner); + self.select(caller.occurrence); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + if let Some(context) = self.scenario_context() { + self.behavior.scenario.restore_caller(context, caller); + } + } else { + // An edit can select a retargeted caller outside the + // definition that remains visible after rejected Back. + if crate::behavior::selected_behavior(self.project(), self.selected) + != Some(context.scenario) + { + self.select(context.scenario); + } + self.status = + "The calling scenario reference is no longer available.".into(); + } + } + CanvasEvent::Select(_, id) => { + if !self.behavior.has_pending() + && crate::behavior::selected_behavior(self.project(), id) + == Some(context.scenario) + { + self.select(id); + self.selected_node = None; + } + } + CanvasEvent::Choose(_, tool) => { + if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() + { + self.status = + "Apply or cancel pending edits before choosing a scenario tool." + .into(); + return Task::none(); + } + self.behavior.scenario.choose_tool(context.scenario, tool); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + mutation => { + if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() + { + self.status = + "Apply or cancel pending edits before changing the scenario." + .into(); + return Task::none(); + } + match mutation { + CanvasEvent::First(_, tool) => { + let empty = match tool { + scenario::Tool::Lifeline => !self + .project() + .children(context.scenario) + .any(|e| e.kind() == Kind::Lifeline), + scenario::Tool::Message | scenario::Tool::Action => { + self.project() + .children(context.scenario) + .any(|e| e.kind() == Kind::Lifeline) + && !self + .project() + .children(context.scenario) + .any(|e| e.kind() == Kind::Occurrence) + } + _ => false, + }; + if !empty { + self.status="This scenario already has content. Use its current authoring tools.".into(); + return Task::none(); + } + let mut state = std::mem::take(&mut self.behavior.scenario); + let result = state.update( + self.project(), + scenario::Event::Start(context.scenario, tool), + ); + self.behavior.scenario = state; + if let Err(error) = result { + self.status = error; + } + } + CanvasEvent::BeginOccurrence { + tool, + slot, + lifeline, + .. + } => { + let mut state = std::mem::take(&mut self.behavior.scenario); + let result = if tool == scenario::Tool::Lifeline { + state + .update( + self.project(), + scenario::Event::Start(context.scenario, tool), + ) + .map(|_| ()) + } else { + state.begin_occurrence( + self.project(), + context, + tool, + slot, + lifeline, + ) + }; + self.behavior.scenario = state; + match result { + Ok(()) => { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.status = "Complete the selected behavior at the highlighted sequence position.".into(); + } + Err(error) => self.status = error, + } + } + CanvasEvent::Begin { + slot, + sender, + receiver, + style, + .. + } => { + let mut state = std::mem::take(&mut self.behavior.scenario); + let result = state.begin_message( + self.project(), + context, + slot, + sender, + receiver, + style, + ); + self.behavior.scenario = state; + match result { + Ok(()) => { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.status = "Choose the message exchange and apply at the highlighted sequence position.".into(); + } + Err(error) => self.status = error, + } + } + CanvasEvent::Reorder { + occurrence, slot, .. + } => { + match scenario::reorder( + self.project(), + context.scenario, + occurrence, + slot, + ) { + Ok(command) => { + if self.execute(command) { + self.selected = occurrence; + self.selected_node = None; + self.refresh_draft(); + self.status = "Occurrence reordered within its sequence · Undo restores the previous order".into(); + } + } + Err(error) => self.status = error, + } + } + _ => unreachable!(), + } + } + } + } + Message::TopologyCanvas(event) => return self.edit_process_canvas(event), + Message::StateDiagram(event) => return self.edit_state_diagram(event), + Message::BehaviorOwner(choice) => { + if self.apply() + && self.project().elements.get(&choice.id).is_some_and(|e| { + e.layer() == self.layer + && (e.kind().is_sequence() || e.kind() == Kind::StateMachine) + }) + { + self.behavior.active_owner = Some(choice.id); + self.behavior.diagram = Default::default(); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.select(choice.id); + self.page = Page::Behavior; + } + } + Message::BehaviorDiagram(choice) => { + if self.apply() + && let Some(owner) = crate::behavior::selected_owner(self) + && self + .project() + .diagrams + .get(&choice.id) + .is_some_and(|d| d.projection.state_machine() == Some(owner)) + { + self.behavior.diagram.diagram = Some(choice.id); + self.behavior.diagram.tool = crate::behavior::state_edit::Tool::Select; + self.behavior.diagram.fit = true; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.fit_state_diagram(); + } + } + Message::PortAnchor(event) => { + use crate::port_edit::Event; + let (captured, node, port, command) = match event { + Event::Commit(c, node, anchor) => ( + c, + node, + anchor.port, + Command::SetPortAnchor { + diagram: c.diagram, + node, + anchor, + }, + ), + Event::Reset(c, node, port) => ( + c, + node, + port, + Command::RemovePortAnchor { + diagram: c.diagram, + node, + port, + }, + ), + }; + if self.route_context() != Some(captured) + || self.page != Page::Diagram + || self.draft_dirty() + || self.source_dirty() + || self.connection.is_some() + { + return Task::none(); + } + let view = project_view(self.project(), captured.diagram); + if !view + .ports + .iter() + .any(|p| p.element == port && p.owner == node) + { + return Task::none(); + } + if self.execute(command) { + self.selected_node = None; + self.status = "Port position updated in this diagram · Undo restores its previous position".into(); + } + } + Message::Connection(event) => self.edit_connection(event), + Message::SourceTree(event) => match event { + crate::source_tree::Event::ToggleFolder(path) => self.source_tree.toggle(&path), + crate::source_tree::Event::OpenFile(path) => { + return self.update(Message::SourceFile(path)); + } + crate::source_tree::Event::FileMenu(path, anchor) => { + let mut request = crate::model_menu::Request::object(self.project().id, anchor); + request.source_file = Some(path); + return self.update(Message::Menu(crate::model_menu::Event::Open(request))); + } + }, + Message::SourceTreeCollapse => self.source_tree.collapse_all(), + Message::SourceTreeLocate => { + self.source_tree.reveal(&self.source_workspace.active); + return iced::widget::operation::scroll_to( + "source-files", + crate::source_tree::active_offset( + &self.source_workspace, + &self.source_diagnostics, + &self.source_tree, + ), + ); + } + Message::Menu(crate::model_menu::Event::Open(request)) => { + // Validate the originating canvas before advancing its input + // epoch. Actions are then generated with the refreshed context, + // while pointer history hidden behind the menu becomes stale. + let Some(opened) = crate::model_menu::Opened::new( + self, + request, + self.menu_sequence.wrapping_add(1), + ) else { + return Task::none(); + }; + self.connection = None; + self.menu_sequence = opened.token; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + let mut request = opened.request; + if request.canvas.is_some() { + request.canvas = if self.page == Page::Behavior { + self.state_context() + } else { + self.route_context() + }; + } + self.model_menu = crate::model_menu::Opened::new(self, request, self.menu_sequence); + } + Message::Menu(crate::model_menu::Event::Close) => { + if self.model_menu.take().is_some() { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + Message::Menu(crate::model_menu::Event::Invoke(token, action)) => { + if self + .model_menu + .as_ref() + .is_none_or(|menu| menu.token != token) + { + return Task::none(); + } + let Some(menu) = self.model_menu.take() else { + return Task::none(); + }; + if menu.token != token || !menu.valid(self) { + return Task::none(); + } + if menu.request.source_file.is_some() { + return self.update(*action); + } + if self.draft_dirty() || self.source_dirty() { + self.status = + "Apply or revert pending edits before using modeling actions.".into(); + return Task::none(); + } + self.select(menu.request.object); + self.selected_node = menu.request.placement; + self.selected_bend = menu.request.bend.map(|i| (menu.request.object, i)); + return self.update(*action); + } + Message::CreateContextElement(kind, child) => { + self.create_kind = kind; + return self.update(Message::Add(child)); + } + Message::Escape => { + if self.behavior.source_back() { + self.status = + "Returned to the unchanged parent form; no source was created.".into(); + return Task::none(); + } + if self.behavior.source_active() { + self.behavior.acknowledge_commit(); + self.status = "Behavior form cancelled; no staged source was created.".into(); + } else if self.behavior.creation.has_pending() { + self.behavior.creation.acknowledge_commit(); + self.status = "Behavior creation cancelled".into(); + } + if self.connection.take().is_some() { + self.status = "Drawing cancelled · Select / move is active".into(); + } + self.source_navigation.open = false; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.selected_bend = None; + } + Message::Guide(event) => { + if self.guide.update(event) { + return iced::widget::operation::snap_to( + crate::guide::ARTICLE_SCROLL_ID, + iced::widget::scrollable::RelativeOffset::START, + ); + } + } + Message::ExactStrings(enabled) => { + if let Some(d) = &mut self.draft { + match d.set_exact(enabled) { + Ok(()) => self.exact_strings = enabled, + Err(e) => self.status = e, + } + } + } + Message::Field(path, value) => { + if let Some(d) = &mut self.draft { + d.input(&path, value) + } + } + Message::FieldText(path, action) => { + if let Some(d) = &mut self.draft { + d.edit_text(&path, action) + } + } + Message::FieldShape(path, value) => { + if let Some(d) = &mut self.draft + && let Err(e) = d.shape(&path, value) + { + self.status = e; + } + } + Message::FieldAppend(path, value) => { + if let Some(draft) = &mut self.draft + && let Err(error) = draft.append(&path, value) + { + self.status = error; + } + } + Message::FieldValueBrowse(path, scope, page) => { + if let Some(draft) = &mut self.draft + && draft + .value + .pointer(&path) + .is_some_and(|row| row.get("expression").is_some()) + { + draft + .value_browsers + .insert(path, crate::forms::ValueBrowser { scope, page }); + } + } + Message::FieldReferenceOpen(_) | Message::FieldReferenceEvent(_) => { + return Task::none(); + } + Message::FieldReferenceBrowse(path, query, page) => { + if let Some(draft) = &mut self.draft { + let page = if draft + .reference_browsers + .get(&path) + .is_some_and(|b| b.query == query) + { + page + } else { + 0 + }; + draft + .reference_browsers + .insert(path, crate::forms::ReferenceBrowser { query, page }); + } + } + Message::FieldValuePath(path, key) => { + if let Some(draft) = &mut self.draft + && let Err(error) = draft.value_path(&path, key) + { + self.status = error; + } + } + Message::FieldVariant(path, value) => { + if let Some(d) = &mut self.draft { + let result = d.materialize().and_then(|current| { + if current + .pointer(&path) + .is_some_and(|v| crate::forms::same_variant(v, &value)) + { + Ok(()) + } else { + d.shape(&path, value) + } + }); + if let Err(e) = result { + self.status = e; + } + } + } + Message::FieldArray(path, index, direction) => { + if let Some(d) = &mut self.draft + && let Err(e) = d.array(&path, index, direction) + { + self.status = e; + } + } + Message::Apply => { + self.apply(); + } + Message::RevertProperties => { + self.refresh_draft(); + self.status = "Property draft reverted".into(); + } + Message::Select(id) => self.select(id), + Message::EditProperties(id) => { + self.select(id); + self.inspector_tab = crate::navigation::InspectorTab::Properties; + } + Message::SelectProject => self.select(self.project().id), + Message::SelectNode(id) => { + if (self.selected_node == Some(id) + && self.active_diagram + .and_then(|diagram| self.project().diagrams.get(&diagram)) + .and_then(|diagram| diagram.nodes.get(&id)) + .is_some_and(|node| self.selected == node.element)) + // Defer a placement draft until release: it may change the + // identity captured by the just-started canvas gesture. + || (self.selected == id && self.draft.as_ref().is_some_and(Draft::dirty)) + { + return Task::none(); + } + let original = self.selected; + if !self.apply() { + return Task::none(); + } + let id = if id == original { self.selected } else { id }; + if let Some(d) = self + .active_diagram + .and_then(|d| self.project().diagrams.get(&d)) + && let Some(node) = d.nodes.get(&id) + { + self.selected = node.element; + self.selected_node = Some(id); + self.refresh_draft(); + } + } + Message::Traceability(event) => { + self.traceability.update(event); + return iced::widget::operation::snap_to( + "traceability-list", + iced::widget::scrollable::RelativeOffset::START, + ); + } + Message::Search(value) => { + self.search = value; + self.explorer_page = 0; + return Task::batch(["model-browser", "model-explorer"].map(|id| { + iced::widget::operation::snap_to( + id, + iced::widget::scrollable::RelativeOffset::START, + ) + })); + } + Message::Page(page) => { + if page != Page::Text || !self.draft_dirty() || self.apply() { + if page == Page::Diagram + && self.active_diagram.is_some_and(|id| { + self.project() + .diagrams + .get(&id) + .is_some_and(|d| d.projection.state_machine().is_some()) + }) + { + // Architecture remains reachable from a state workspace. + // Its picker can still open a saved state view explicitly. + self.active_diagram = self + .project() + .diagrams + .values() + .filter(|d| { + d.layer == self.layer && d.projection.state_machine().is_none() + }) + .min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id)) + .map(|d| d.id); + self.fit(); + } + if page == Page::Behavior && self.page != Page::Behavior { + let previous_state_view = self.state_view_identity(); + if let Some(owner) = + crate::behavior::selected_behavior(self.project(), self.selected) + { + self.behavior.active_owner = Some(owner); + } + self.behavior.active_owner = crate::behavior::selected_owner(self); + self.reset_state_view_after_navigation(previous_state_view); + } + self.page = page; + self.selected_node = if page == Page::Behavior { + None + } else { + self.selected_architecture_placement() + }; + if page == Page::Text { + return self.request_lint(); + } + } + } + Message::CollapseExplorer(id) => { + if !self.explorer_collapsed.remove(&id) { + self.explorer_collapsed.insert(id); + } + } + Message::ExplorerGroup(group) => { + if !self.explorer_groups_collapsed.remove(&group) { + self.explorer_groups_collapsed.insert(group); + } + } + Message::ExplorerCollapseAll => { + self.explorer_collapsed = self + .project() + .elements + .values() + .filter_map(|e| e.parent) + .collect(); + self.explorer_groups_collapsed = crate::explorer::Group::ALL.into_iter().collect(); + self.search.clear(); + return iced::widget::operation::snap_to( + "model-explorer", + iced::widget::scrollable::RelativeOffset::START, + ); + } + Message::ExplorerLocate => { + self.search.clear(); + self.reveal_explorer_selection(); + let offset = crate::explorer_view::selection_offset(self); + return iced::widget::operation::scroll_to("model-explorer", offset); + } + Message::Explore(id) => { + if self.apply() { + self.explorer_focus = id.filter(|id| self.project().elements.contains_key(id)); + self.explorer_group = self + .explorer_focus + .and_then(|id| self.explorer_index.group(id)); + self.search.clear(); + self.explorer_page = 0; + self.page = Page::Catalog; + self.select(self.explorer_focus.unwrap_or(self.project().id)); + return iced::widget::operation::snap_to( + "model-browser", + iced::widget::scrollable::RelativeOffset::START, + ); + } + } + Message::BrowseGroup(group) => { + self.explorer_focus = None; + self.explorer_group = Some(group); + self.search.clear(); + self.explorer_page = 0; + self.page = Page::Catalog; + return iced::widget::operation::snap_to( + "model-browser", + iced::widget::scrollable::RelativeOffset::START, + ); + } + Message::ExplorerPage(page) => { + self.explorer_page = page; + return iced::widget::operation::snap_to( + "model-browser", + iced::widget::scrollable::RelativeOffset::START, + ); + } + Message::StartTransition => { + if self.apply() { + match crate::workflow::Workflow::transition(self.project(), self.selected) { + Ok(w) => self.workflow = Some(w), + Err(e) => self.status = e, + } + } + } + Message::StartSemanticReview(selected) => { + if self.apply() { + match crate::semantic_review::State::new(self.project(), selected) { + Ok(state) => { + self.workflow = + Some(crate::workflow::Workflow::Semantic(Box::new(state))) + } + Err(error) => self.status = error, + } + } + } + Message::Workflow(event) => { + if matches!(event, crate::workflow::Event::Cancel) { + self.workflow = None; + return Task::none(); + } + if let Some(mut workflow) = self.workflow.take() { + if let crate::workflow::Workflow::Semantic(state) = &mut workflow { + match event { + crate::workflow::Event::Semantic(event) => { + state.update(self.project(), *event) + } + crate::workflow::Event::Apply => { + let focus = state.target(self.project()); + match state.command(self.project()) { + Ok(command) => { + if self.execute(command) { + if let Some(target) = focus + && self.focus_after_review(target, None) + && self.page == Page::Diagram + { + self.fit(); + } + self.status = "Semantic review applied · model, source and baseline form one undoable change".into(); + return Task::none(); + } + state.error = Some(self.status.clone()); + } + Err(error) => { + self.status = error.clone(); + state.error = Some(error); + } + } + } + _ => {} + } + self.workflow = Some(workflow); + return Task::none(); + } + let stay_in_catalog = self.page == Page::Catalog + && matches!(&workflow, crate::workflow::Workflow::Ports(_)); + let transition_focus = match &workflow { + crate::workflow::Workflow::Ports(state) => Some(state.focus()), + crate::workflow::Workflow::Transition(t) => t + .request + .sources + .first() + .and_then(|source| t.plan.changes.iter().find(|c| c.source == *source)) + .map(|c| c.target), + _ => None, + }; + let behavior_transition = matches!(&workflow, + crate::workflow::Workflow::Transition(t) + if t.request.sources.first().is_some_and(|source| + crate::behavior::selected_behavior(self.project(), *source).is_some())); + let previous_behavior_context = self.state_view_identity(); + let requested_destination = match &workflow { + crate::workflow::Workflow::Transition(t) => t.request.destination_diagram, + _ => None, + }; + match workflow.update(self.project(), event) { + Ok(Some(mut candidate)) => { + let focus = transition_focus + .or_else(|| { + candidate + .relations + .keys() + .find(|id| !self.project().relations.contains_key(id)) + .copied() + }) + .or_else(|| { + candidate + .traces + .keys() + .find(|id| !self.project().traces.contains_key(id)) + .copied() + }); + let behavior_owner = focus + .filter(|_| behavior_transition) + .and_then(|id| crate::behavior::selected_behavior(&candidate, id)); + let destination = focus.and_then(|id| { + candidate.layer_of(id).and_then(|layer| { + requested_destination + .filter(|view| { + candidate.diagrams.get(view).is_some_and(|d| { + (d.layer == layer + || d.projection == ProjectionKind::Traceability) + && d.nodes + .values() + .any(|node| node.element == id) + }) + }) + .or_else(|| { + preferred_diagram( + &candidate, + self.active_diagram, + layer, + id, + ) + }) + }) + }); + if let (None, Some(id), Some(diagram)) = + (behavior_owner, focus, destination) + { + reveal_port_owner(&mut candidate, diagram, id); + } + if self.execute(Command::Replace(candidate)) { + if let Some(id) = focus { + self.focus_after_review(id, destination); + } + if let Some(owner) = behavior_owner { + self.selected_node = None; + self.behavior.active_owner = Some(owner); + self.page = Page::Behavior; + if self.reset_state_view_after_navigation( + previous_behavior_context, + ) { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } else if !stay_in_catalog { + self.page = Page::Diagram; + self.fit(); + } + self.status="Reviewed change applied · Undo restores the complete operation".into(); + } else { + self.workflow = Some(workflow); + } + } + Ok(None) => self.workflow = Some(workflow), + Err(e) => { + self.status = e; + self.workflow = Some(workflow); + } + } + } + } + Message::Layer(layer) => { + if self.apply() { + let previous_state_view = self.state_view_identity(); + self.layer = layer; + self.create_kind = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + self.active_diagram = self + .project() + .diagrams + .values() + .filter(|d| d.layer == layer) + .min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id)) + .map(|d| d.id); + self.selected = self.project().id; + self.selected_node = None; + self.behavior.active_owner = crate::behavior::selected_owner(self); + self.reset_state_view_after_navigation(previous_state_view); + self.refresh_draft(); + self.fit(); + } + } + Message::Data(event) => { + if self.data_state.update(&event) { + return Task::none(); + } + if self.apply() { + match crate::data_view::command(self.project(), self.layer, event) { + Ok(command) => { + self.execute(command); + } + Err(error) => self.status = error, + } + } + } + Message::CreateKind(kind) => self.create_kind = kind, + Message::Add(child) => { + if self.apply() { + self.add_element(child); + } + } + Message::ElementKind(kind) => { + if let Some(d) = &mut self.draft { + let result = d + .materialize() + .and_then(|v| { + serde_json::from_value::(v).map_err(|e| e.to_string()) + }) + .and_then(|element| { + if element.kind() == kind { + return Ok(()); + } + let body = ElementBody::new(kind, element.layer()) + .ok_or("Kind is unavailable in this layer.")?; + d.shape("/body", json!(body)) + }); + if let Err(e) = result { + self.status = e; + } + } + } + Message::RelationKind(kind) => { + if let Some(d) = &mut self.draft { + let result = d + .materialize() + .and_then(|v| { + serde_json::from_value::(v).map_err(|e| e.to_string()) + }) + .and_then(|relation| { + if relation.kind() == kind { + return Ok(()); + } + d.shape( + "/body", + json!(RelationBody::new( + kind, + relation.source(), + relation.target() + )), + ) + }); + if let Err(e) = result { + self.status = e; + } + } + } + Message::AddRelation => { + if self.apply() { + self.add_relation(); + } + } + Message::PortAction(action) => { + if self.apply() { + match crate::port_workflow::State::new(self.project(), self.selected, action) { + Ok(state) => { + self.inspector_tab = crate::navigation::InspectorTab::Properties; + self.workflow = Some(crate::workflow::Workflow::Ports(Box::new(state))) + } + Err(error) => self.status = error, + } + } + } + Message::AddRelationKind(kind, incoming) => { + if self.apply() { + self.workflow = Some(crate::workflow::Workflow::relationship_kind( + self.project(), + self.selected, + self.layer, + kind, + incoming, + )); + } + } + Message::InspectorTab(tab) => self.inspector_tab = tab, + Message::Behavior(event) => { + if matches!(&event, behavior::Event::Creation(behavior::creation::Event::Open(layer)) if *layer != self.layer) + { + self.status = + "The active layer changed. Choose New in the current layer.".into(); + return Task::none(); + } + let creating = matches!( + &event, + behavior::Event::Creation(behavior::creation::Event::Open(..)) + ); + let focus_name = matches!( + &event, + behavior::Event::Creation(behavior::creation::Event::Choose(..)) + ); + if creating + && (self.behavior.has_pending() || self.source_dirty() || self.draft_dirty()) + { + self.status = "Apply or cancel the current draft before starting a new behavior. Your inputs are retained.".into(); + return Task::none(); + } + if (event.starts_form() || (event.is_source() && !event.is_source_back())) + && (self.source_dirty() || self.draft_dirty()) + { + self.status="Apply or cancel the other editors before starting this form. Your inputs are retained.".into(); + return Task::none(); + } + let source_was_open = self.behavior.source_open(); + if event.is_submit() && (self.source_dirty() || self.draft_dirty()) { + self.status = "Revert the other drafts before applying the behavior form. Your inputs are retained.".into(); + return Task::none(); + } + let mut state = std::mem::take(&mut self.behavior); + let result = state.update(self.project(), event); + self.behavior = state; + match result { + Ok(Some(command)) => { + if self.execute(command) { + self.behavior.acknowledge_commit(); + if self.topology_context().is_some() { + return self.reveal_process_selection(); + } + } + } + Ok(None) => { + if !source_was_open && self.behavior.source_open() { + return iced::widget::operation::focus( + behavior::source_draft::NAME_INPUT, + ); + } + if focus_name { + return iced::widget::operation::focus(behavior::creation::NAME_INPUT); + } + } + Err(error) => self.status = error, + } + } + Message::RevealInDiagram(diagram, selected) => { + let original = self.selected; + if !self.apply() { + return Task::none(); + } + let selected = if selected == original { + self.selected + } else { + selected + }; + let Some(view) = self.project().diagrams.get(&diagram) else { + return Task::none(); + }; + let layer = view.layer; + let endpoints = self + .project() + .relations + .get(&selected) + .map(|r| vec![r.source(), r.target()]) + .unwrap_or_else(|| vec![selected]) + .into_iter() + .map(|id| { + self.project() + .elements + .get(&id) + .filter(|e| e.kind() == Kind::Port) + .and_then(|e| e.parent) + .unwrap_or(id) + }) + .collect::>(); + let nodes = view + .nodes + .values() + .filter(|n| endpoints.contains(&n.element) || n.id == selected) + .map(|n| n.id) + .collect::>(); + let commands = view + .nodes + .values() + .filter(|n| { + n.collapsed && { + let children = oa_core::projection::descendant_nodes( + self.project(), + diagram, + n.id, + ); + nodes.iter().any(|id| children.contains(id)) + } + }) + .map(|n| { + let mut node = n.clone(); + node.collapsed = false; + Command::EditNode { + diagram, + original: node.id, + node, + } + }) + .collect::>(); + if !commands.is_empty() && !self.execute(Command::Batch(commands)) { + return Task::none(); + } + self.active_diagram = Some(diagram); + self.layer = layer; + self.selected = selected; + self.page = Page::Diagram; + self.refresh_draft(); + self.fit(); + } + Message::AddDiagram => { + if self.apply() { + let id = Id::new_v4(); + let p = self.project(); + let d = Diagram { + id, + alias: p.fresh_alias(&format!("{}_view", self.layer), "view", id), + name: format!("{} architecture", self.layer), + description: String::new(), + layer: self.layer, + projection: ProjectionKind::Allocation, + routes: Vec::new(), + view_options: Default::default(), + nodes: Default::default(), + }; + if self.execute(Command::CreateDiagram(d)) { + self.active_diagram = Some(id); + self.page = Page::Diagram; + } + } + } + Message::DuplicateDiagram(source) => { + if !self.project().diagrams.contains_key(&source) { + self.status = "The saved view to duplicate no longer exists.".into(); + return Task::none(); + } + let original = self.selected; + if self.apply() { + let source = if source == original { + self.selected + } else { + source + }; + match crate::view_copy::command(self.project(), source) { + Ok(command) => { + if self.execute(command) { + self.active_diagram = Some(self.selected); + self.layer = self.project().diagrams[&self.selected].layer; + self.selected_node = None; + self.selected_bend = None; + self.page = Page::Diagram; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.route_state_diagram_workspace(); + if self.page == Page::Diagram { + self.fit(); + } + self.status = + "View copied; engineering elements are shared.".into(); + } + } + Err(error) => self.status = error, + } + } + } + Message::AddArtifact => { + if self.apply() { + let id = Id::new_v4(); + let p = self.project(); + let a = ArtifactLink { + id, + alias: p.fresh_alias("artifact", "artifact", id), + name: "Development artifact".into(), + description: String::new(), + owner: self.selected, + relation: ArtifactRelation::References, + uri: "./artifacts/design.md".into(), + revision: None, + fragment: None, + }; + self.execute(Command::CreateArtifact(a)); + } + } + Message::AddTrace => { + if self.apply() { + self.add_trace(); + } + } + Message::Command(command) => { + let semantic_selection = match &command { + Command::EditNode { diagram, node, .. } + if self.state_context().is_some_and(|c| c.diagram == *diagram) => + { + Some(node.element) + } + _ => None, + }; + let original = self.selected; + if self.apply() { + let command = match command { + Command::Layout(id) if id == original => Command::Layout(self.selected), + Command::MoveStep(id, from, to) if id == original => { + Command::MoveStep(self.selected, from, to) + } + command => command, + }; + if self.execute(command) + && let Some(element) = semantic_selection + { + self.selected = element; + self.refresh_draft(); + } + } + } + Message::Delete => { + if self.selected != self.project().id && self.apply() { + self.delete_pending = true; + } + } + Message::ConfirmDelete => { + if !self.delete_pending || self.behavior.has_pending() { + return Task::none(); + } + self.delete_pending = false; + self.execute(Command::Delete(self.selected)); + } + Message::CancelDelete => self.delete_pending = false, + Message::Undo | Message::Redo => { + let redo = matches!(message, Message::Redo); + if self.apply() { + let changed = if redo { + self.document.redo() + } else { + self.document.undo() + }; + if changed { + self.sync(); + } + self.status = if !changed && redo { + "Nothing to redo" + } else if !changed { + "Nothing to undo" + } else if redo { + "Model change redone" + } else { + "Model change undone" + } + .into(); + } + } + Message::ShortcutUndo(redo) => { + return self.handle(if self.page == Page::Text { + if redo { + Message::SourceRedo + } else { + Message::SourceUndo + } + } else if redo { + Message::Redo + } else { + Message::Undo + }); + } + Message::Diagram(choice) => { + // Reject a stale picker event before applying unrelated property input. + if !self.project().diagrams.contains_key(&choice.id) { + self.status = "Diagram no longer exists.".into(); + return Task::none(); + } + let previous_diagram = self.active_diagram; + let follow_view = previous_diagram == Some(self.selected); + if self.apply() { + // Apply may have changed the current diagram's UUID. A picker + // event for that same view must follow its new identity. + let diagram = if previous_diagram == Some(choice.id) { + self.active_diagram.unwrap_or(choice.id) + } else { + choice.id + }; + self.active_diagram = Some(diagram); + if let Some(d) = self.project().diagrams.get(&diagram) { + self.layer = d.layer; + } + if follow_view { + self.selected = diagram; + } + self.selected_node = None; + self.refresh_draft(); + self.fit(); + } + } + Message::Projection(projection) => { + if self.apply() + && let Some(mut d) = self + .active_diagram + .and_then(|id| self.project().diagrams.get(&id)) + .cloned() + { + d.projection = projection; + self.execute(Command::EditDiagram(d.id, d)); + self.fit(); + } + } + Message::PlaceSelected => { + if self.apply() + && let Some(diagram) = self.active_diagram + { + let selected = self.selected; + if let Some(node) = self + .project() + .diagrams + .get(&diagram) + .and_then(|d| d.nodes.values().find(|n| n.element == self.selected)) + .map(|n| n.id) + { + self.selected_node = Some(node); + self.status = "Element is already shown in this diagram".into(); + return self.handle(Message::RevealInDiagram(diagram, selected)); + } + let mut p = self.project().clone(); + let node = p.place(diagram, self.selected, Position { x: 60., y: 60. }); + if let Some(n) = p + .diagrams + .get(&diagram) + .and_then(|d| d.nodes.get(&node)) + .cloned() + { + let mut commands = vec![Command::CreateNode { diagram, node: n }]; + for owner in p.diagrams[&diagram].nodes.values().filter(|n| { + n.collapsed + && projection::descendant_nodes(&p, diagram, n.id).contains(&node) + }) { + let mut expanded = owner.clone(); + expanded.collapsed = false; + commands.push(Command::EditNode { + diagram, + original: owner.id, + node: expanded, + }); + } + if self.execute(Command::Batch(commands)) { + self.selected_node = Some(node); + return self.handle(Message::RevealInDiagram(diagram, selected)); + } + } + } + } + Message::HideSelected => { + if self.apply() + && let Some(node) = self.selected_node + { + self.execute(Command::Delete(node)); + self.selected_node = None; + } + } + Message::ToggleCollapse => { + if self.apply() + && let Some(diagram) = self.active_diagram + && let Some(mut n) = self + .selected_node + .and_then(|id| self.project().diagrams.get(&diagram)?.nodes.get(&id)) + .cloned() + { + n.collapsed = !n.collapsed; + let selected = self.selected; + if self.execute(Command::EditNode { + diagram, + original: n.id, + node: n, + }) { + self.selected = selected; + self.refresh_draft(); + } + } + } + Message::Route(event) => self.edit_route(event), + Message::MoveNode(node, position) => { + let original = self.selected; + if self.apply() + && let Some(diagram) = self.active_diagram + { + let node = if node == original { + self.selected + } else { + node + }; + if self.execute(Command::MoveNode { + diagram, + node, + position, + with_descendants: true, + }) && let Some(placement) = + self.project().diagrams[&diagram].nodes.get(&node) + { + // A drag edits a placement, but the canvas still selects + // the engineering element represented by that placement. + self.selected = placement.element; + self.refresh_draft(); + } + } + } + Message::Zoom(factor) => { + self.fit_to_view = false; + self.zoom = (self.zoom * factor).clamp(0.0001, 3.); + } + Message::Pan(delta) => { + self.fit_to_view = false; + self.pan += delta; + } + Message::Fit => self.fit(), + Message::SourceEdit(action) => { + self.source_jump = None; + let before = self.source_editor.text(); + self.source_editor.perform(action); + let after = self.source_editor.text(); + if after != before { + self.source_navigation.refresh(&after); + self.source_workspace + .edit(after.clone(), self.source_editor.cursor()); + self.source_snapshot = Arc::from(after); + self.source_version += 1; + self.source_diagnostics.clear(); + let version = self.source_version; + return Task::perform( + async move { + tokio::time::sleep(std::time::Duration::from_millis(350)).await; + version + }, + Message::Lint, + ); + } + } + Message::SourceFile(path) => { + if self + .source_workspace + .switch(&path, self.source_editor.cursor()) + { + self.show_source_file(true); + } + } + Message::SourceNewPath(path) => self.source_workspace.new_path = path, + Message::SourceCreateFile => { + if let Err(error) = self.create_source_file() { + self.status = error; + } else { + return self.request_lint(); + } + } + Message::SourceCloseFile(path) => { + if self.source_workspace.discard_detached(&path) { + self.source_version += 1; + self.prune_closed_files(!self.document.dirty()); + self.show_source_file(true); + } + } + Message::SourceLoadIncludes => return self.load_includes(), + Message::SourceRecover => { + if !self.source_workspace.applied && self.source_workspace.has_edits() { + self.status = + "Revert or preserve your source drafts before reloading recovered files." + .into(); + return Task::none(); + } + if let Some(path) = self.pending_recovery.clone() { + self.busy = true; + return Task::perform( + async move { + tokio::task::spawn_blocking(move || recover_snapshot(path)) + .await + .map_err(|e| e.to_string())? + }, + Message::SourceRecovered, + ); + } + } + Message::SourceRecovered(result) => { + self.busy = false; + match result { + Ok(receipt) => { + if self.pending_recovery.as_ref() != Some(&receipt.target) { + return Task::none(); + } + self.pending_recovery = None; + self.source_diagnostics + .retain(|issue| issue.diagnostic.code != "pending_save"); + match receipt.disk { + Ok(disk) if !self.source_workspace.applied => { + self.open_source_project(disk) + } + Ok(disk) => { + // Recovering an old Save As target must not redirect + // the source project that was subsequently saved elsewhere. + if self.path.is_none() + || self.path.as_ref() == Some(&receipt.target) + { + self.path = Some(receipt.target); + self.disk_project = Some(disk); + } + self.status = "Interrupted disk save recovered. Current model and drafts are retained; Save all can be retried.".into(); + } + Err(error) => { + self.status = format!( + "Disk recovery completed; files could not be reloaded: {error}" + ) + } + } + } + Err(error) => self.status = error, + } + } + Message::IncludesLoaded(version, result) => { + self.busy = false; + if version == self.source_version { + match result { + Ok(disk) => { + if let Some(target) = recovery_target(&disk) { + self.pending_recovery = Some(target); + } + self.source_workspace + .discovered(&disk.bundle, disk.reachable.clone()); + self.source_version += 1; + self.source_diagnostics = disk.diagnostics.clone(); + self.disk_project = Some(disk); + self.status = "Included files loaded; existing drafts retained.".into(); + return self.request_lint(); + } + Err(error) => self.status = error, + } + } + } + Message::Lint(version) => { + if version == self.source_version { + return self.request_lint(); + } + } + Message::Linted(version, result) => { + if self.lint_running != Some(version) { + return Task::none(); + } + self.lint_running = None; + if version == self.source_version { + match result { + Ok(diagnostics) => self.source_diagnostics = diagnostics, + Err(error) => self.status = format!("Source checks failed: {error}"), + } + } + return self.start_pending_lint(); + } + Message::SourceDiagnostic(index) => { + if let Some(issue) = self.source_diagnostics.get(index) { + self.jump_to_source(SourceLocation { + path: issue.path.clone(), + span: issue.diagnostic.span, + }); + } + } + Message::SourceRelated(location) => self.jump_to_source(location), + Message::SourceViewport => { + if let Some(cursor) = self.source_jump.take() { + self.source_editor.move_to(cursor); + if self.source_select_line && cursor.selection.is_none() { + self.source_editor.perform(text_editor::Action::SelectLine); + } + return iced::widget::operation::focus("source-editor"); + } + } + Message::ApplySource => { + self.apply_source(); + } + Message::RevertSource => { + self.source_workspace.revert_active(); + self.show_source_file(true); + self.source_version += 1; + self.status = "This file's draft reverted; other files retained.".into(); + return self.request_lint(); + } + Message::SourceUndo | Message::SourceRedo => { + let redo = matches!(message, Message::SourceRedo); + if self.source_workspace.undo(redo) { + self.show_source_file(true); + self.source_version += 1; + self.source_diagnostics.clear(); + return self.request_lint(); + } + } + Message::SourceForSelection => { + if self.apply() { + self.page = Page::Text; + if let Some(location) = self + .document + .compiled() + .index + .declarations + .get(&self.selected) + .cloned() + { + self.jump_to_source(location); + self.status = "Source declaration selected.".into(); + } + return self.request_lint(); + } + } + Message::SourceNavigation(event) => { + use crate::source_navigation::Event; + match event { + Event::Open(go_to_line) => { + if self.page != Page::Text && !self.apply() { + return Task::none(); + } + self.page = Page::Text; + self.source_navigation.open = true; + self.source_navigation.refresh(&self.source_editor.text()); + return iced::widget::operation::focus(if go_to_line { + "source-line" + } else { + "source-find" + }); + } + Event::Close => self.source_navigation.open = false, + Event::Query(query) => { + self.source_navigation.query = query; + self.source_navigation.refresh(&self.source_editor.text()); + } + Event::Next(backwards) if self.page == Page::Text => { + self.source_jump = self.source_navigation.next(backwards); + if self.source_jump.is_none() { + self.status = "No matching source text.".into(); + } + } + Event::Line(line) => self.source_navigation.line = line, + Event::Go => match self + .source_navigation + .destination(self.source_editor.line_count()) + { + Ok(cursor) => { + self.source_jump = Some(cursor); + self.source_select_line = true; + } + Err(error) => self.status = error, + }, + _ => {} + } + } + Message::Request(pending) => { + if self.dirty() { + self.pending = Some(pending); + } else { + return self.proceed(pending); + } + } + Message::Cancel => { + self.pending = None; + self.continue_after_save = false; + } + Message::Discard => { + if let Some(p) = self.pending.take() { + return self.proceed(p); + } + } + Message::SaveAndContinue => { + self.continue_after_save = true; + return self.handle(Message::Save(false)); + } + Message::Save(save_as) => { + if !save_as && self.hosted.session.is_some() { + return self.save_hosted(); + } + if !self.apply_behavior() || !self.apply_source() || !self.apply() { + self.continue_after_save = false; + self.pending = None; + return Task::none(); + } + if !save_as && let Some(path) = self.path.clone() { + return self.save_to(path); + } + return self.choose_file(FileOperation::Save); + } + Message::SavePath(path) => { + self.busy = false; + if let Some(path) = path { + return self.save_to(path); + } + self.continue_after_save = false; + self.status = "Save cancelled".into(); + } + Message::Saved(result) => { + self.busy = false; + match result { + Ok(Some(receipt)) => { + if receipt.document != self.canvas_document { + return Task::none(); + } + if let Some(disk) = receipt.disk { + if self.pending_recovery.as_ref() + == Some(&disk.root.join(&disk.bundle.entry)) + { + self.pending_recovery = None; + } + if receipt.snapshot.bundle.entry != self.document.bundle().entry { + if receipt.revision != self.document.revision() + || self.source_dirty() + { + self.status = format!( + "Snapshot saved to {}. Newer edits retain the current source location.", + receipt.path.display() + ); + self.continue_after_save = false; + return Task::none(); + } + if let Err(error) = self + .document + .adopt_saved_entry(&receipt.snapshot, receipt.revision) + { + self.status = error.to_string(); + self.continue_after_save = false; + return Task::none(); + } + self.sync_source(); + } + self.disk_project = Some(disk); + self.path = Some(receipt.path); + self.hosted.session = None; + self.hosted.review = None; + self.hosted.uncertain = None; + self.document.mark_saved(receipt.snapshot); + self.remember_local(); + if !self.dirty() { + self.retire_checkpoint(); + } + self.prune_closed_files(!self.document.dirty()); + self.status = "All source files saved".into(); + } else { + self.status = format!( + "JSON exported to {}. Source files and their save state are retained.", + receipt.path.display() + ); + self.continue_after_save = false; + } + if self.continue_after_save { + self.continue_after_save = false; + if let Some(p) = self.pending.take() { + return self.proceed(p); + } + } + } + Ok(None) => { + self.status = "Save cancelled".into(); + self.continue_after_save = false; + } + Err(e) => { + if e.document != self.canvas_document { + return Task::none(); + } + if let Some(target) = e.recovery { + self.pending_recovery = Some(target); + } + self.status = e.message; + if self.pending_recovery.is_some() { + self.page = Page::Text; + } + self.continue_after_save = false; + } + } + } + Message::Loaded(result) => { + self.busy = false; + match result { + Ok(Some(LoadedSnapshot::Legacy(path, decoded, _bytes))) => { + self.reset(decoded.project, None); + let name = self.project().name.clone(); + if let Err(error) = self.home.remember(&path, &name) { + self.home.notice = Some(error); + } + self.migration = decoded.migration; + if self.migration.is_some() { + self.status = "Imported earlier schema · review migration details in Model checks before saving".into(); + self.page = Page::Validation; + } else { + self.status = format!( + "Imported {}. Save as an Arcadia source project to retain file structure and comments.", + path.display() + ); + } + } + Ok(Some(LoadedSnapshot::Source(disk))) => self.open_source_project(disk), + Ok(None) => self.status = "Open cancelled".into(), + Err(e) => self.status = e, + } + } + Message::FilePath(value) => { + if let Some(p) = &mut self.file_prompt { + p.path = value; + p.overwrite = false; + } + } + Message::CancelFile => { + self.file_prompt = None; + self.continue_after_save = false; + } + Message::SubmitFile => { + if let Some(prompt) = &mut self.file_prompt { + if prompt.path.trim().is_empty() { + self.status = "Enter a file path.".into(); + return Task::none(); + } + let path = PathBuf::from(prompt.path.trim()); + if prompt.operation == FileOperation::Export + && path.exists() + && !prompt.overwrite + { + prompt.overwrite = true; + return Task::none(); + } + let operation = prompt.operation; + self.file_prompt = None; + return match operation { + FileOperation::Open => self.load_from(path), + FileOperation::Save => self.save_to(path), + FileOperation::Export => self.export_to(path), + }; + } + } + Message::Export => { + if self.apply() { + return self.choose_file(FileOperation::Export); + } + } + Message::Exported(result) => { + self.busy = false; + self.status = match result { + Ok(true) => "Diagram exported as SVG".into(), + Ok(false) => "Export cancelled".into(), + Err(e) => e, + }; + } + Message::WindowResized(size) => { + // The canvas can retain its capped size during a real window + // resize. Invalidate any sequence pointer/slot gesture anyway. + if self.window_size != size + && (self.scenario_context().is_some() || self.topology_context().is_some()) + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + self.window_size = size; + } + Message::CanvasResized(size) => { + let first_measurement = self.canvas_size.is_none(); + let size_changed = self.canvas_size != Some(size); + self.canvas_size = Some(size); + // Modal overlays can remount the sensor and repeat on_show at + // the same size. Keep the viewport stable after model edits. + if first_measurement || (size_changed && self.fit_to_view) { + self.fit(); + } + } + } + Task::none() + } + fn add_element(&mut self, child: bool) { + if self.create_kind == Kind::Port { + self.status = "Select an SA function or participant, then choose a typed port action in Properties.".into(); + return; + } + let allocated = child + && self.create_kind.is_work() + && self + .project() + .elements + .get(&self.selected) + .is_some_and(|e| e.layer() == self.layer && e.kind().is_performer()); + let parent = if child && !allocated { + Some(self.selected) + } else { + None + }; + let Some(e) = self.project().make_element( + self.create_kind, + self.layer, + &format!("New {}", self.create_kind.label().to_lowercase()), + parent, + ) else { + return; + }; + let id = e.id; + let mut commands = vec![Command::CreateElement(e)]; + let mut diagram_target = self.active_diagram; + let preferred = if self.create_kind.is_work() || self.create_kind.is_performer() { + Some(ProjectionKind::Allocation) + } else if ProjectionKind::Capabilities.includes(self.create_kind) { + Some(ProjectionKind::Capabilities) + } else { + None + }; + if self.page == Page::Diagram + && self + .active_diagram + .and_then(|id| self.project().diagrams.get(&id)) + .is_some_and(|d| !d.projection.includes(self.create_kind)) + && let Some(preferred) = preferred + { + diagram_target = self + .project() + .diagrams + .values() + .find(|d| d.layer == self.layer && d.projection == preferred) + .map(|d| d.id); + if diagram_target.is_none() { + let view = Id::new_v4(); + commands.push(Command::CreateDiagram(Diagram { + id: view, + alias: self.project().fresh_alias("engineering_view", "view", view), + name: format!("{} {preferred}", self.layer), + description: String::new(), + layer: self.layer, + projection: preferred, + routes: Vec::new(), + view_options: Default::default(), + nodes: Default::default(), + })); + diagram_target = Some(view); + } + } + if allocated { + commands.push(Command::Connect( + RelationKind::Allocation, + "Performs".into(), + self.selected, + id, + )); + } + if self.create_kind == Kind::Occurrence + && let Some(owner) = parent.and_then(|id| self.project().elements.get(&id)) + && let Some(steps) = owner.steps() + { + let mut steps = steps.clone(); + steps.push(id); + commands.push(Command::SetSteps(owner.id, steps)); + } + if self.page == Page::Diagram + && self.create_kind != Kind::Port + && let Some(diagram) = diagram_target + && self + .project() + .diagrams + .get(&diagram) + .is_none_or(|d| d.projection.includes(self.create_kind)) + { + let mut p = self.project().clone(); + for command in &commands { + if let Command::CreateDiagram(d) = command { + p.diagrams.insert(d.id, d.clone()); + } + } + p.elements.insert( + id, + match &commands[0] { + Command::CreateElement(e) => e.clone(), + _ => unreachable!(), + }, + ); + let owner = if allocated { + Some(self.selected) + } else { + parent + }; + // Insertion measures an expanded owner path and commits that reveal + // with the new child, so folded contents cannot overlap the new row. + if let Some(owner) = owner { + let mut expanded = p.diagrams[&diagram].clone(); + for node in expanded.nodes.values_mut() { + node.collapsed = false; + } + let expanded_view = oa_core::projection::project_diagram(&p, &expanded); + let mut current = expanded_view + .nodes + .iter() + .find(|n| n.element == owner) + .map(|n| n.id); + while let Some(id) = current { + let node = p + .diagrams + .get_mut(&diagram) + .unwrap() + .nodes + .get_mut(&id) + .unwrap(); + if node.collapsed { + node.collapsed = false; + commands.push(Command::EditNode { + diagram, + original: id, + node: node.clone(), + }); + } + current = expanded_view + .nodes + .iter() + .find(|n| n.id == id) + .and_then(|n| n.visual_parent); + } + } + // Reserve the expanded size of folded child containers so inserting + // a sibling does not overlap their contents when they are unfolded. + let mut expanded = p.diagrams[&diagram].clone(); + for node in expanded.nodes.values_mut() { + node.collapsed = false; + } + let view = oa_core::projection::project_diagram(&p, &expanded); + let position = + if let Some(container) = view.nodes.iter().find(|n| Some(n.element) == owner) { + let y = view + .nodes + .iter() + .filter(|n| n.visual_parent == Some(container.id)) + .map(|n| n.position.y + n.size.height + 16.) + .fold(container.position.y + 64., f32::max); + Position { + x: container.position.x + 24., + y, + } + } else { + Position { + x: view + .nodes + .iter() + .map(|n| n.position.x + n.size.width + 240.) + .fold(60., f32::max), + y: 60., + } + }; + let node = p.place(diagram, id, position); + commands.push(Command::CreateNode { + diagram, + node: p.diagrams[&diagram].nodes[&node].clone(), + }); + } + if self.page == Page::Diagram + && let Some(diagram) = diagram_target + && let Ok(candidate) = execute_batch(self.project(), &commands) + { + commands.extend(crate::diagram::insertion_adjustments( + &candidate, diagram, id, + )); + } + if self.execute(Command::Batch(commands)) { + self.active_diagram = diagram_target; + self.selected = id; + self.inspector_tab = crate::navigation::InspectorTab::Properties; + self.refresh_draft(); + if self.page == Page::Diagram { + self.fit(); + } + self.status = "Element created · enter its engineering name and properties".into(); + } + } + fn add_relation(&mut self) { + self.workflow = Some(crate::workflow::Workflow::relationship( + self.project(), + self.selected, + self.layer, + )); + } + fn add_trace(&mut self) { + match crate::workflow::Workflow::manual_trace(self.project(), self.selected) { + Ok(workflow) => self.workflow = Some(workflow), + Err(e) => self.status = e, + } + } + fn proceed(&mut self, pending: Pending) -> Task { + self.pending = None; + match pending { + Pending::Recent => self + .recent_target + .take() + .map_or_else(Task::none, |path| self.load_from(path)), + Pending::Recover(id) => self.load_checkpoint(id), + Pending::HostedOpen => self.open_hosted(), + Pending::New => { + self.reset(Project::empty(), None); + Task::none() + } + Pending::Demo => { + self.reset(Project::demo(), None); + Task::none() + } + Pending::Walkthrough => match crate::home::walkthrough_path() { + Ok(path) => self.load_from(path), + Err(error) => { + self.status = error; + Task::none() + } + }, + Pending::Open => self.choose_file(FileOperation::Open), + Pending::Close(id) => { + self.retire_checkpoint(); + if self.recovery.writing { + self.checkpoint_close = Some(id); + self.busy = true; + Task::none() + } else { + window::close(id) + } + } + } + } + fn choose_file(&mut self, operation: FileOperation) -> Task { + if cfg!(target_os = "linux") || operation == FileOperation::Export { + self.file_prompt = Some(FilePrompt { + operation, + path: self + .path + .clone() + .unwrap_or_else(|| { + PathBuf::from(if operation == FileOperation::Export { + "architecture.svg" + } else { + "system.arcadia" + }) + }) + .with_extension(if operation == FileOperation::Export { + "svg" + } else { + "arcadia" + }) + .to_string_lossy() + .into_owned(), + overwrite: false, + }); + return Task::none(); + } + self.busy = true; + if operation == FileOperation::Open { + return Task::perform( + async { + let file = rfd::AsyncFileDialog::new() + .add_filter("Arcadia model", &["arcadia", "json"]) + .pick_file() + .await; + match file { + None => Ok(None), + Some(file) => { + let path = file.path().to_path_buf(); + tokio::task::spawn_blocking(move || read_snapshot(path).map(Some)) + .await + .map_err(|e| e.to_string())? + } + } + }, + Message::Loaded, + ); + } + let name = self.source_workspace.entry.clone(); + Task::perform( + async move { + let file = rfd::AsyncFileDialog::new() + .add_filter("Arcadia source", &["arcadia"]) + .add_filter("Model JSON", &["json"]) + .set_file_name(name) + .save_file() + .await; + file.map(|file| file.path().to_path_buf()) + }, + Message::SavePath, + ) + } + fn load_from(&mut self, path: PathBuf) -> Task { + self.busy = true; + Task::perform( + async move { + tokio::task::spawn_blocking(move || read_snapshot(path).map(Some)) + .await + .map_err(|e| e.to_string())? + }, + Message::Loaded, + ) + } + fn save_to(&mut self, path: PathBuf) -> Task { + if !path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("json")) + { + let result = path + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| "Choose a portable .arcadia entry file name.".to_owned()) + .and_then(|entry| { + self.document + .check_save_entry(entry) + .map_err(|e| e.to_string()) + }); + if let Err(error) = result { + self.status = error; + self.continue_after_save = false; + return Task::none(); + } + } + self.busy = true; + let snapshot = self.document.snapshot(); + let document = self.canvas_document; + let revision = self.document.revision(); + let previous = self.disk_project.clone(); + let failed_target = path.clone(); + Task::perform( + async move { + tokio::task::spawn_blocking(move || { + write_snapshot(document, revision, snapshot, path, previous).map(Some) + }) + .await + .map_err(|e| save_failure(document, &failed_target, e.to_string()))? + }, + Message::Saved, + ) + } + fn export_to(&mut self, path: PathBuf) -> Task { + let Some(diagram) = self.active_diagram else { + self.status = "Create a diagram first.".into(); + return Task::none(); + }; + let svg = crate::diagram::export_svg(self.project(), diagram); + self.busy = true; + Task::perform( + async move { + tokio::task::spawn_blocking(move || { + std::fs::write(path, svg) + .map(|()| true) + .map_err(|e| e.to_string()) + }) + .await + .map_err(|e| e.to_string())? + }, + Message::Exported, + ) + } + pub fn issue_count(&self) -> usize { + self.model_issues.len() + } + + fn open_source_project(&mut self, disk: DiskProject) { + let pending_recovery = recovery_target(&disk); + let path = disk.root.join(&disk.bundle.entry); + let candidate = if !disk + .diagnostics + .iter() + .any(|issue| issue.diagnostic.severity == oa_core::validation::Severity::Error) + { + Document::from_bundle(disk.reachable_bundle(), LIMITS) + } else { + Err(crate::document::DocumentError::Source( + disk.diagnostics.clone(), + )) + }; + match candidate { + Ok(document) => { + self.reset(document.project().clone(), Some(path)); + self.document = document; + self.source_workspace = Workspace::new(self.document.bundle(), true); + self.source_workspace + .discovered(&disk.bundle, disk.reachable.clone()); + self.show_source_file(false); + self.source_diagnostics = self.document.compiled().diagnostics.clone(); + self.migration = self.document.compiled().decoded.migration.clone(); + if self.migration.is_some() { + self.status = "Imported earlier source schema. Review Model checks; Save all explicitly writes the upgraded source while retaining comments and includes.".into(); + self.page = Page::Validation; + } else { + self.status = + "Source project opened. Included files and comments are preserved.".into(); + } + } + Err(error) => { + // Installing a local repair workspace changes its destination, + // even while the last valid graphical model stays visible. A + // failed/cancelled Open that installs nothing retains its binding. + self.hosted.leave_document(); + self.canvas_document = Id::new_v4(); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.path = Some(path); + self.source_workspace = Workspace::new(&disk.bundle, false); + self.source_workspace.reachable = disk.reachable.clone(); + self.show_source_file(false); + self.source_diagnostics = error.diagnostics().to_vec(); + self.source_version += 1; + self.lint_requested = None; + self.page = Page::Text; + self.refresh_draft(); + self.status = "Source needs repair. Loaded drafts are retained; the graphical model is unchanged.".into(); + } + } + self.pending_recovery = pending_recovery; + self.disk_project = Some(disk); + if self.source_workspace.applied { + self.remember_local(); + } + } + + fn prune_closed_files(&mut self, prune_expectations: bool) { + if let Some(disk) = &mut self.disk_project { + let retain = |path: &String| { + self.document.bundle().files.contains_key(path) + || self.source_workspace.files.contains_key(path) + }; + disk.bundle.files.retain(|path, _| retain(path)); + if prune_expectations { + disk.expectations.retain(|path, _| retain(path)); + disk.reachable.retain(retain); + } + } + } + + fn jump_to_source(&mut self, location: SourceLocation) { + self.source_workspace + .switch(&location.path, self.source_editor.cursor()); + if self.source_workspace.active != location.path { + self.status = format!( + "{} has not been loaded. Use Load includes or create the missing file.", + location.path + ); + return; + } + self.show_source_file(false); + let source = self.source_workspace.text(); + let mut offset = location.span.start.min(source.len()); + while !source.is_char_boundary(offset) { + offset -= 1; + } + let prefix = &source[..offset]; + let line = prefix.bytes().filter(|b| *b == b'\n').count(); + let column = prefix + .rfind('\n') + .map_or(offset, |start| offset - start - 1); + self.source_jump = Some(text_editor::Cursor { + position: text_editor::Position { line, column }, + selection: None, + }); + self.source_select_line = true; + } + + fn create_source_file(&mut self) -> Result<(), String> { + let path = self.source_workspace.new_path.trim().to_owned(); + if source_project::resolve_include("", &path).as_deref() != Ok(path.as_str()) { + return Err("Use a relative portable path such as sa/functions.arcadia.".into()); + } + if self.source_workspace.files.contains_key(&path) { + return Err("This file is already open.".into()); + } + if self.source_workspace.files.len() >= LIMITS.max_files { + return Err("The project file limit has been reached.".into()); + } + let bundle = self.source_workspace.bundle(); + let already_included = bundle.files.iter().any(|(p, t)| { + source_project::inspect_includes(p, t, p == &bundle.entry, LIMITS) + .is_ok_and(|includes| includes.iter().any(|include| include.target == path)) + }); + let mut changed = bundle.clone(); + if !already_included { + if self.source_dirty() { + return Err( + "Apply current drafts first, or add an include for the new file in the entry." + .into(), + ); + } + let at = self.document.compiled().index.project_body.span.end; + let entry = changed + .files + .get_mut(&changed.entry) + .ok_or("The entry file is missing.")?; + entry.insert_str( + at, + &format!( + "\n include {};\n", + serde_json::to_string(&path).map_err(|e| e.to_string())? + ), + ); + self.source_workspace + .switch(&bundle.entry, self.source_editor.cursor()); + self.source_workspace + .edit(entry.clone(), self.source_editor.cursor()); + } + changed.files.insert(path.clone(), String::new()); + self.source_workspace + .discovered(&changed, changed.files.keys().cloned().collect()); + self.source_workspace + .switch(&path, self.source_editor.cursor()); + self.source_workspace.new_path.clear(); + self.show_source_file(true); + self.source_version += 1; + self.status = "Source file created. Add declarations, then Apply all.".into(); + Ok(()) + } + + fn load_includes(&mut self) -> Task { + let Some(disk) = self.disk_project.clone() else { + self.status = "Save the source project first to establish its include directory. New files can be created here.".into(); + return Task::none(); + }; + let overlays = self.source_workspace.bundle().files; + let version = self.source_version; + self.busy = true; + Task::perform( + async move { + tokio::task::spawn_blocking(move || { + source_files::discover_missing(&disk, &overlays, LIMITS) + .map_err(|e| e.to_string()) + }) + .await + .map_err(|e| e.to_string())? + }, + move |result| Message::IncludesLoaded(version, result), + ) + } + + fn request_lint(&mut self) -> Task { + self.lint_requested = Some(self.source_version); + self.start_pending_lint() + } + + fn start_pending_lint(&mut self) -> Task { + if self.lint_running.is_some() { + return Task::none(); + } + let Some(version) = self.lint_requested.take() else { + return Task::none(); + }; + if version != self.source_version { + return Task::none(); + } + self.lint_running = Some(version); + let source = self.source_workspace.bundle(); + Task::perform( + async move { + tokio::task::spawn_blocking(move || source_workspace::lint(&source)) + .await + .map_err(|error| error.to_string()) + }, + move |result| Message::Linted(version, result), + ) + } + pub fn route_context(&self) -> Option { + Some(crate::route_edit::Context { + document: self.canvas_document, + diagram: self.active_diagram?, + revision: self.document.revision(), + epoch: self.canvas_epoch, + }) + } + fn edit_connection(&mut self, event: crate::connection_edit::Event) { + use crate::connection_edit::{Event, Session}; + let Some(context) = self.route_context() else { + return; + }; + if matches!(event, Event::Choose(None)) { + self.connection = None; + self.status = + "Select · drag elements, ports and connection routes to arrange the diagram".into(); + return; + } + if self.page != Page::Diagram || self.source_dirty() || self.draft_dirty() { + self.status = "Apply or revert pending edits before drawing a connection.".into(); + return; + } + match event { + Event::Begin(tool, source) => { + self.edit_connection(Event::Choose(Some(tool))); + if let Some(context) = self.route_context() { + self.edit_connection(Event::Endpoint(context, source)); + } + } + Event::Choose(Some(tool)) => { + if !crate::connection_tool::Tool::for_layer(self.layer).contains(&tool) { + return; + } + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + let context = self.route_context().unwrap(); + self.connection = Some(Session::new( + self.project(), + &project_view(self.project(), context.diagram), + context, + tool, + )); + self.selected_bend = None; + self.status = tool.hint().into(); + } + Event::Endpoint(captured, id) => { + if captured != context { + return; + } + let Some(mut session) = self.connection.take() else { + return; + }; + if session.context != context { + return; + } + if !session.candidates.contains(&id) { + self.status = + "Choose a highlighted compatible endpoint · Esc cancels drawing".into(); + self.connection = Some(session); + return; + } + if let Some(source) = session.source { + match session.tool.command(self.project(), source, id) { + Ok(command) => { + if self.execute(command) { + self.selected_node = None; + self.inspector_tab = Default::default(); + self.status = format!( + "{} created · edit its name and carried items in Properties · Undo restores the complete operation", + session.tool.label() + ); + } else { + self.connection = Some(session); + } + } + Err(error) => { + self.status = error.to_string(); + self.connection = Some(session); + } + } + } else { + session.start( + self.project(), + &project_view(self.project(), context.diagram), + id, + ); + self.select(id); + self.status = if session.candidates.is_empty() { + "No compatible targets are visible. Expand containers or show a compatible endpoint in this view; Esc cancels drawing.".into() + } else { + format!( + "From {} · choose a highlighted target · Esc cancels", + self.project().named(id) + ) + }; + self.connection = Some(session); + } + } + Event::Restart(captured) => { + if captured == context + && let Some(session) = &self.connection + { + self.edit_connection(Event::Choose(Some(session.tool))); + } + } + Event::Choose(None) => {} + } + } + fn edit_route(&mut self, event: crate::route_edit::Event) { + use crate::route_edit::{Event, insert_pin, stored_route}; + if self.connection.is_some() { + return; + } + let Some(context) = self.route_context() else { + return; + }; + if self.source_dirty() || self.draft_dirty() { + self.status = "Apply or revert your draft before editing a connection route.".into(); + return; + } + let diagram = &self.project().diagrams[&context.diagram]; + let view = project_view(self.project(), context.diagram); + let Some(projected_edge) = view.edges.iter().find(|e| e.id == self.selected) else { + return; + }; + if projection::edge_endpoint_placements(self.project(), diagram, self.selected).is_some_and( + |(source, target)| source != projected_edge.source || target != projected_edge.target, + ) { + self.status = + "Expand the endpoint containers to edit this connection’s saved route.".into(); + return; + } + let mut route = stored_route(diagram, self.selected); + let mut selected_bend = self + .selected_bend + .filter(|(id, _)| *id == self.selected) + .map(|(_, i)| i); + let command = match event { + Event::SelectBend(id, index) => { + if id == self.selected && index < route.waypoints.len() { + self.selected_bend = Some((id, index)); + } + return; + } + Event::Commit(expected, changed, bend) => { + if expected != context || changed.edge != self.selected { + self.status = + "Route gesture cancelled because the model or view changed.".into(); + return; + } + route = changed; + selected_bend = bend; + Command::SetEdgeRoute { + diagram: context.diagram, + route, + } + } + Event::AddBend | Event::AddBendAt(_) => { + if route.waypoints.len() >= 128 { + return; + } + let geometry = oa_core::routing::route_diagram(self.project(), diagram, &view); + let edge = &geometry.edges[&self.selected]; + // Mandatory port leads are inside the card's clearance. A pin + // starts beyond those leads, including coincident short stubs. + let segment = edge + .points + .windows(2) + .enumerate() + .filter(|(index, _)| *index > 0 && index + 2 < edge.points.len()) + .map(|(_, span)| span) + .max_by(|a, b| { + let len = + |s: &[Position]| (s[1].x - s[0].x).abs() + (s[1].y - s[0].y).abs(); + len(a).total_cmp(&len(b)) + }); + let point = if let Event::AddBendAt(point) = event { + if !point.x.is_finite() || !point.y.is_finite() { + return; + } + point + } else if let Some(segment) = segment { + Position { + x: (segment[0].x + segment[1].x) / 2., + y: (segment[0].y + segment[1].y) / 2., + } + } else if let Some(stub) = edge.points.get(1) { + *stub + } else { + return; + }; + selected_bend = Some(insert_pin(&mut route, edge, point)); + Command::SetEdgeRoute { + diagram: context.diagram, + route, + } + } + Event::RemoveBend => { + let Some(index) = selected_bend.filter(|i| *i < route.waypoints.len()) else { + return; + }; + route.waypoints.remove(index); + selected_bend = None; + Command::SetEdgeRoute { + diagram: context.diagram, + route, + } + } + Event::Reset => { + selected_bend = None; + Command::RemoveEdgeRoute { + diagram: context.diagram, + edge: self.selected, + } + } + }; + let selected = self.selected; + if self.execute(command) { + self.selected = selected; + self.selected_bend = selected_bend.map(|index| (selected, index)); + self.refresh_draft(); + self.status = "Connection route updated · Undo restores the previous route".into(); + } + } + pub fn fit(&mut self) { + self.fit_to_view = true; + let view = self + .active_diagram + .map(|id| project_view(self.project(), id)) + .unwrap_or_default(); + if view.nodes.is_empty() { + self.zoom = 1.; + self.pan = iced::Vector::new(30., 30.); + return; + } + let geometry = oa_core::routing::route_diagram( + self.project(), + &self.project().diagrams[&self.active_diagram.unwrap()], + &view, + ); + let left = geometry.bounds.position.x; + let top = geometry.bounds.position.y; + let right = left + geometry.bounds.size.width; + let bottom = top + geometry.bounds.size.height; + let size = self.canvas_size.unwrap_or_else(|| { + iced::Size::new( + self.window_size.width - 230. - 320., + self.window_size.height - 284., + ) + }); + let width = (size.width - 48.).max(1.); + let height = (size.height - 48.).max(1.); + self.zoom = (width / (right - left).max(1.)) + .min(height / (bottom - top).max(1.)) + .clamp(0.0001, 1.5); + self.pan = iced::Vector::new( + 24. + (width - (right - left) * self.zoom) / 2. - left * self.zoom, + 24. + (height - (bottom - top) * self.zoom) / 2. - top * self.zoom, + ); + } +} + +fn preferred_diagram(p: &Project, active: Option, layer: Layer, element: Id) -> Option { + if let Some(owner) = p + .elements + .get(&element) + .filter(|e| e.kind() == Kind::Port) + .and_then(|e| e.parent) + { + return p + .diagrams + .values() + .filter(|d| d.layer == layer) + .min_by_key(|d| { + let view = project_view(p, d.id); + ( + !d.projection.includes(p.elements[&owner].kind()), + !view.ports.iter().any(|port| port.element == element), + !d.nodes.values().any(|node| node.element == owner), + active != Some(d.id), + ) + }) + .map(|d| d.id); + } + p.diagrams + .values() + .filter(|d| d.layer == layer) + .min_by_key(|d| { + ( + !p.elements + .get(&element) + .is_none_or(|e| d.projection.includes(e.kind())), + !d.nodes.values().any(|n| n.element == element), + d.projection != ProjectionKind::Allocation, + ) + }) + .map(|d| d.id) +} +fn reveal_port_owner(p: &mut Project, diagram: Id, element: Id) { + let Some(owner) = p + .elements + .get(&element) + .filter(|e| e.kind() == Kind::Port) + .and_then(|e| e.parent) + else { + return; + }; + let Some(owner_node) = p.diagrams[&diagram] + .nodes + .values() + .find(|n| n.element == owner) + .map(|n| n.id) + else { + return; + }; + let unfold: Vec<_> = p.diagrams[&diagram] + .nodes + .values() + .filter(|n| { + n.collapsed && projection::descendant_nodes(p, diagram, n.id).contains(&owner_node) + }) + .map(|n| n.id) + .collect(); + for id in unfold { + p.diagrams + .get_mut(&diagram) + .unwrap() + .nodes + .get_mut(&id) + .unwrap() + .collapsed = false; + } +} + +fn write_snapshot( + document: Id, + revision: u64, + snapshot: Arc, + path: PathBuf, + previous: Option, +) -> Result { + let target = path.clone(); + write_snapshot_inner(document, revision, snapshot, path, previous) + .map_err(|message| save_failure(document, &target, message)) +} + +fn save_failure(document: Id, path: &std::path::Path, message: String) -> SaveFailure { + // Discovery is read-only, bounded and checks physical source ownership. Do + // not infer journal names from an error string or recover inside a save. + let recovery = path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia")) + .then(|| source_files::load_project(path, LIMITS).ok()) + .flatten() + .and_then(|disk| recovery_target(&disk)); + SaveFailure { + document, + message, + recovery, + } +} + +fn recovery_target(disk: &DiskProject) -> Option { + disk.diagnostics + .iter() + .any(|issue| issue.diagnostic.code == "pending_save") + .then(|| disk.root.join(&disk.bundle.entry)) +} + +fn recover_snapshot(target: PathBuf) -> Result { + if !source_files::recover_project(&target, LIMITS).map_err(|e| e.to_string())? { + return Err(format!( + "No pending save was found at {}. Your model and drafts are retained.", + target.display() + )); + } + let disk = source_files::load_project(&target, LIMITS).map_err(|e| e.to_string()); + Ok(RecoverySnapshot { target, disk }) +} + +fn write_snapshot_inner( + document: Id, + revision: u64, + mut snapshot: Arc, + path: PathBuf, + previous: Option, +) -> Result { + let is_json = path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("json")); + let disk = if is_json { + if path.exists() { + return Err( + "The JSON destination already exists. Choose a new file to preserve it.".into(), + ); + } + persistence::save_checked(&path, &snapshot.decoded.project, None) + .map_err(|e| e.to_string())?; + None + } else { + let entry = path + .file_name() + .and_then(|p| p.to_str()) + .ok_or("Choose a portable .arcadia entry file name.")?; + if source_project::resolve_include("", entry).as_deref() != Ok(entry) { + return Err("Choose a portable .arcadia entry file name.".into()); + } + if entry != snapshot.bundle.entry { + let mut bundle = snapshot.bundle.clone(); + if bundle.files.contains_key(entry) { + return Err("That entry name is already used by an included file.".into()); + } + let text = bundle + .files + .remove(&bundle.entry) + .ok_or("The accepted entry is missing.")?; + bundle.entry = entry.into(); + bundle.files.insert(entry.into(), text); + snapshot = Arc::new( + source_project::compile_bundle(&bundle, LIMITS) + .map_err(|errors| crate::document::DocumentError::Source(errors).to_string())?, + ); + } + let previous = previous.filter(|disk| { + path.parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(std::path::Path::new(".")) + .canonicalize() + .ok() + .as_ref() + == Some(&disk.root) + && entry == disk.bundle.entry + }); + let mut disk = match previous { + Some(disk) => disk, + None => source_files::new_project(&path, LIMITS).map_err(|e| e.to_string())?, + }; + for path in snapshot.bundle.files.keys() { + if !disk.expectations.contains_key(path) { + source_files::expect_new_file(&mut disk, path, LIMITS) + .map_err(|e| e.to_string())?; + } + } + Some(source_files::save_project(&disk, &snapshot, LIMITS).map_err(|e| e.to_string())?) + }; + Ok(FileSnapshot { + document, + revision, + snapshot, + disk, + path, + }) +} + +fn read_snapshot(path: PathBuf) -> Result { + use std::io::Read; + let is_source = path + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia")); + if is_source { + // The entry is written last. An interrupted first save can therefore + // have a valid journal without an entry file to open yet. + let disk = source_files::load_project(&path, LIMITS).map_err(|e| e.to_string())?; + if recovery_target(&disk).is_none() + && let Some(source_files::DiskExpectation::Bytes(bytes)) = + disk.expectations.get(&disk.bundle.entry) + && let Ok(decoded) = persistence::decode_with_report_limited(bytes, LIMITS.model) + // Source1/2 require the historical single-document adapters. + // Source3+ must retain its authored bundle and token migration. + && decoded.migration.as_ref().is_some_and(|report| report.from_schema <= 2) + { + return Ok(LoadedSnapshot::Legacy( + path, + Box::new(decoded), + Arc::from(bytes.clone()), + )); + } + return Ok(LoadedSnapshot::Source(disk)); + } + if !std::fs::symlink_metadata(&path) + .map_err(|e| e.to_string())? + .is_file() + { + return Err("Open a regular model file; links and special files are not supported.".into()); + } + let mut bytes = Vec::new(); + std::fs::File::open(&path) + .map_err(|e| e.to_string())? + .take(LIMITS.model.max_source_bytes as u64 + 1) + .read_to_end(&mut bytes) + .map_err(|e| e.to_string())?; + if bytes.len() > LIMITS.model.max_source_bytes { + return Err("The model exceeds the source byte limit.".into()); + } + let decoded = + persistence::decode_with_report_limited(&bytes, LIMITS.model).map_err(|e| e.to_string())?; + Ok(LoadedSnapshot::Legacy( + path, + Box::new(decoded), + Arc::from(bytes), + )) +} + +#[cfg(test)] +#[path = "app/save_as_tests.rs"] +mod save_as_tests; + +#[cfg(test)] +mod source_project_tests { + use super::*; + + struct Directory(PathBuf); + impl Directory { + fn new() -> Self { + let path = std::env::temp_dir().join(format!("arcadia-source-ui-{}", Id::new_v4())); + std::fs::create_dir(&path).unwrap(); + Self(path.canonicalize().unwrap()) + } + } + impl Drop for Directory { + fn drop(&mut self) { + let temp = std::env::temp_dir().canonicalize().unwrap(); + assert_eq!(self.0.parent(), Some(temp.as_path())); + assert!( + self.0 + .file_name() + .unwrap() + .to_string_lossy() + .starts_with("arcadia-source-ui-") + ); + std::fs::remove_dir_all(&self.0).unwrap(); + } + } + fn send(app: &mut App, message: Message) { + let _ = app.update(message); + } + fn edit(app: &mut App, text: &str) { + send(app, Message::SourceEdit(text_editor::Action::SelectAll)); + send( + app, + Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( + Arc::new(text.into()), + ))), + ); + } + fn save(app: &mut App, path: PathBuf) -> Result<(), String> { + let receipt = write_snapshot( + app.canvas_document, + app.document.revision(), + app.document.snapshot(), + path, + app.disk_project.clone(), + ) + .map_err(|error| error.message)?; + send(app, Message::Saved(Ok(Some(receipt)))); + Ok(()) + } + + #[test] + fn source3_native_open_preserves_report_comments_nesting_and_save_reopen() { + let original = include_str!("../../oa-core/tests/fixtures/v3-data-contract.arcadia"); + for split in [false, true] { + let directory = Directory::new(); + let path = directory.0.join("project.arcadia"); + let mut authored = std::collections::BTreeMap::new(); + if split { + let start = original.find("{\n").unwrap() + 2; + let end = original.rfind('}').unwrap(); + authored.insert( + "project.arcadia", + format!( + "{} include \"dictionary.arcadia\";\n}}\n", + &original[..start] + ), + ); + authored.insert("dictionary.arcadia", original[start..end].to_owned()); + } else { + authored.insert("project.arcadia", original.to_owned()); + } + for (file, source) in &authored { + std::fs::write(directory.0.join(file), source).unwrap(); + } + let mut app = App::with_project(Project::empty()); + send( + &mut app, + Message::Loaded(Ok(Some(read_snapshot(path.clone()).unwrap()))), + ); + assert_eq!(app.migration.as_ref().unwrap().from_schema, 3); + assert_eq!(app.page, Page::Validation); + assert_eq!(app.path, Some(path.clone())); + assert_eq!(app.document.bundle().files.len(), authored.len()); + assert!( + app.dirty(), + "accepted source migration needs explicit saving" + ); + for (file, source) in &authored { + assert_eq!( + std::fs::read_to_string(directory.0.join(file)).unwrap(), + *source + ); + } + let text = app + .document + .bundle() + .files + .values() + .cloned() + .collect::(); + assert!(text.contains("// Reused record parameters retain their own identities.")); + assert!(text.contains("// Frozen authored schema3 compatibility input.")); + let packet = app + .project() + .elements + .values() + .find(|e| e.alias == "packet") + .unwrap() + .id; + let origin = app + .project() + .elements + .values() + .find(|e| e.alias == "origin") + .unwrap() + .id; + assert_eq!( + app.document.compiled().index.lexical_owners.get(&origin), + Some(&packet) + ); + let before = app.document.snapshot(); + save(&mut app, path.clone()).unwrap(); + assert!(!app.dirty()); + for (file, source) in &before.bundle.files { + assert_eq!( + std::fs::read_to_string(directory.0.join(file)).unwrap(), + *source + ); + } + let mut reopened = App::with_project(Project::empty()); + send( + &mut reopened, + Message::Loaded(Ok(Some(read_snapshot(path).unwrap()))), + ); + assert!(reopened.migration.is_none()); + assert_eq!(reopened.project(), app.project()); + assert_eq!(reopened.document.bundle(), app.document.bundle()); + assert!(!reopened.dirty()); + } + } + + fn pending_journal(directory: &Directory, before: Option<&str>) -> (PathBuf, PathBuf) { + let path = directory.0.join("project.arcadia"); + let journal = directory.0.join(".arcadia-save-project.arcadia.journal"); + std::fs::create_dir(&journal).unwrap(); + let metadata = json!({"version":1,"entry":"project.arcadia","files":[{ + "path":"project.arcadia","before":before.is_some(),"write":true + }]}); + std::fs::write( + journal.join("journal.json"), + serde_json::to_vec(&metadata).unwrap(), + ) + .unwrap(); + if let Some(before) = before { + std::fs::write(&path, before).unwrap(); + std::fs::write(journal.join("0.before"), before).unwrap(); + } + std::fs::write( + journal.join("0.after"), + "arcadia 3; project p \"After\" {}\n", + ) + .unwrap(); + (path, journal) + } + + #[test] + fn recovery_open_finds_first_save_journal_without_an_entry_file() { + let directory = Directory::new(); + let (path, journal) = pending_journal(&directory, None); + let mut app = App::with_project(Project::empty()); + let before = app.document.snapshot(); + send( + &mut app, + Message::Loaded(Ok(Some(read_snapshot(path.clone()).unwrap()))), + ); + assert_eq!(app.pending_recovery.as_ref(), Some(&path)); + assert_eq!(app.page, Page::Text); + assert!(!app.source_workspace.applied); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + assert!(journal.exists()); + send(&mut app, Message::SourceRecover); + assert!(app.busy); + send( + &mut app, + Message::SourceRecovered(recover_snapshot(path.clone())), + ); + assert!(app.pending_recovery.is_none()); + assert!(!journal.exists()); + assert!(!path.exists()); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + } + + #[test] + fn recovery_after_first_save_failure_retains_target_then_retries_without_reentering_path() { + let directory = Directory::new(); + let (path, journal) = pending_journal(&directory, None); + let mut app = App::with_project(Project::empty()); + let before = app.document.snapshot(); + let error = write_snapshot( + app.canvas_document, + app.document.revision(), + app.document.snapshot(), + path.clone(), + None, + ) + .unwrap_err(); + assert_eq!(error.recovery.as_ref(), Some(&path)); + send(&mut app, Message::Saved(Err(error))); + assert!(app.path.is_none()); + assert_eq!(app.pending_recovery.as_ref(), Some(&path)); + assert_eq!(app.page, Page::Text); + send(&mut app, Message::SourceRecover); + assert!( + app.busy, + "Recovery must use its own target when App.path is absent" + ); + send( + &mut app, + Message::SourceRecovered(recover_snapshot(path.clone())), + ); + assert_eq!(app.path.as_ref(), Some(&path)); + assert!(app.pending_recovery.is_none()); + assert!(!journal.exists()); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + save(&mut app, path.clone()).unwrap(); + assert!(path.is_file()); + assert!(!app.dirty()); + } + + #[test] + fn recovery_target_survives_edit_lint_apply_and_rejected_recovery_until_success() { + let directory = Directory::new(); + let initial = "arcadia 3; project p \"Before\" {}\n"; + let (path, journal) = pending_journal(&directory, Some(initial)); + let mut app = App::with_project(Project::empty()); + send( + &mut app, + Message::Loaded(Ok(Some(read_snapshot(path.clone()).unwrap()))), + ); + edit(&mut app, &format!("{initial}// Added rationale\n")); + assert!(app.source_diagnostics.is_empty()); + assert_eq!(app.pending_recovery.as_ref(), Some(&path)); + let version = app.source_version; + send(&mut app, Message::Lint(version)); + let diagnostics = source_workspace::lint(&app.source_workspace.bundle()); + send(&mut app, Message::Linted(version, Ok(diagnostics))); + assert_eq!(app.pending_recovery.as_ref(), Some(&path)); + assert!(app.apply_source()); + let accepted = app.document.snapshot(); + assert_eq!(app.pending_recovery.as_ref(), Some(&path)); + std::fs::write(&path, "External file edit that must survive").unwrap(); + send( + &mut app, + Message::SourceRecovered(recover_snapshot(path.clone())), + ); + assert_eq!(app.pending_recovery.as_ref(), Some(&path)); + assert!(journal.exists()); + assert_eq!( + std::fs::read_to_string(&path).unwrap(), + "External file edit that must survive" + ); + assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); + std::fs::write(&path, initial).unwrap(); + send( + &mut app, + Message::SourceRecovered(recover_snapshot(path.clone())), + ); + assert!(app.pending_recovery.is_none()); + assert!(!journal.exists()); + assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert!(app.source_editor.text().contains("Added rationale")); + } + + #[test] + fn recovery_of_previous_save_as_target_does_not_redirect_current_project() { + let directory = Directory::new(); + let (target, journal) = pending_journal(&directory, None); + let mut app = App::with_project(Project::empty()); + let error = write_snapshot( + app.canvas_document, + app.document.revision(), + app.document.snapshot(), + target.clone(), + None, + ) + .unwrap_err(); + send(&mut app, Message::Saved(Err(error))); + let destination = directory.0.join("another.arcadia"); + save(&mut app, destination.clone()).unwrap(); + assert_eq!(app.pending_recovery.as_ref(), Some(&target)); + let saved = app.document.snapshot(); + send(&mut app, Message::SourceRecovered(recover_snapshot(target))); + assert!(app.pending_recovery.is_none()); + assert!(!journal.exists()); + assert_eq!(app.path.as_ref(), Some(&destination)); + assert_eq!( + app.disk_project.as_ref().unwrap().bundle.entry, + "another.arcadia" + ); + assert!(Arc::ptr_eq(&saved, &app.document.snapshot())); + save(&mut app, destination).unwrap(); + } + #[test] + fn two_file_apply_graphical_edit_exact_undo_save_as_and_reopen() { + let directory = Directory::new(); + let mut project = Project::empty(); + let activity = project + .add_element(Kind::Activity, Layer::Oa, "Assess incident", None) + .unwrap(); + let mut app = App::with_project(project); + let fragment = app.document.compiled().index.declarations[&activity] + .path + .clone(); + send(&mut app, Message::SourceFile(fragment.clone())); + let edited = format!( + "// Engineering rationale: preserve this comment.\n{}", + app.source_editor + .text() + .replace("Assess incident", "Review incident") + ); + edit(&mut app, &edited); + let entry = app.source_workspace.entry.clone(); + send(&mut app, Message::SourceFile(entry.clone())); + let entry_text = format!("{}\n// Project rationale.\n", app.source_editor.text()); + edit(&mut app, &entry_text); + assert!(app.apply_source(), "{}", app.status); + let accepted = app.document.snapshot(); + send( + &mut app, + Message::Command(Command::Rename(activity, "Evaluate incident".into())), + ); + assert_eq!(app.project().elements[&activity].name, "Evaluate incident"); + assert!(app.document.bundle().files[&fragment].starts_with("// Engineering rationale")); + send(&mut app, Message::Undo); + assert_eq!(app.document.bundle(), &accepted.bundle); + send(&mut app, Message::Redo); + let model = app.project().clone(); + let path = directory.0.join("study.arcadia"); + save(&mut app, path.clone()).unwrap(); + assert!(!app.dirty()); + assert_eq!(app.source_workspace.entry, "study.arcadia"); + assert!(!app.source_workspace.files.contains_key(&entry)); + let mut reopened = App::with_project(Project::empty()); + send( + &mut reopened, + Message::Loaded(Ok(Some(read_snapshot(path).unwrap()))), + ); + assert_eq!( + reopened.project(), + &model, + "{}: {:?}", + reopened.status, + reopened.source_diagnostics + ); + assert_eq!(reopened.document.bundle(), app.document.bundle()); + assert!(!reopened.dirty()); + } + #[test] + fn inactive_invalid_draft_blocks_graphical_changes_and_diagnostic_switches_files() { + let mut project = Project::empty(); + let activity = project + .add_element(Kind::Activity, Layer::Oa, "Assess incident", None) + .unwrap(); + let mut app = App::with_project(project.clone()); + let fragment = app.document.compiled().index.declarations[&activity] + .path + .clone(); + send(&mut app, Message::SourceFile(fragment.clone())); + edit(&mut app, "? invalid source"); + let entry = app.source_workspace.entry.clone(); + send(&mut app, Message::SourceFile(entry)); + send(&mut app, Message::Page(Page::Diagram)); + send( + &mut app, + Message::Command(Command::Rename(activity, "Must not happen".into())), + ); + assert_eq!(app.project(), &project); + assert!(!app.apply_source()); + assert_eq!(app.source_diagnostics[0].path, fragment); + send(&mut app, Message::SourceDiagnostic(0)); + assert_eq!(app.source_workspace.active, fragment); + assert_eq!(app.source_editor.text(), "? invalid source"); + send(&mut app, Message::RevertSource); + assert!(!app.source_dirty()); + assert_eq!(app.project(), &project); + } + #[test] + fn missing_include_open_keeps_old_model_and_can_be_repaired_and_saved() { + let directory = Directory::new(); + let compiled = source_project::compile_bundle( + &source_project::format_bundle(&Project::empty()), + LIMITS, + ) + .unwrap(); + let mut entry = compiled.bundle.files[&compiled.bundle.entry].clone(); + entry.insert_str( + compiled.index.project_body.span.end, + "\n include \"missing.arcadia\";\n", + ); + let path = directory.0.join(&compiled.bundle.entry); + std::fs::write(&path, &entry).unwrap(); + let mut app = App::with_project(Project::demo()); + let original = app.project().clone(); + send( + &mut app, + Message::Loaded(Ok(Some(read_snapshot(path.clone()).unwrap()))), + ); + assert_eq!(app.project(), &original); + assert!(!app.source_workspace.applied); + assert_eq!(app.source_editor.text(), entry); + send(&mut app, Message::SourceNewPath("missing.arcadia".into())); + send(&mut app, Message::SourceCreateFile); + assert!(app.apply_source(), "{}", app.status); + assert_eq!(app.project(), &compiled.decoded.project); + save(&mut app, path).unwrap(); + assert_eq!( + std::fs::read(directory.0.join("missing.arcadia")).unwrap(), + Vec::::new() + ); + assert!(!app.dirty()); + } + #[test] + fn closed_detached_file_keeps_first_save_conflict_then_can_be_reloaded_fresh() { + let directory = Directory::new(); + let mut app = App::with_project(Project::empty()); + send(&mut app, Message::SourceNewPath("spare.arcadia".into())); + send(&mut app, Message::SourceCreateFile); + assert!(app.apply_source()); + let path = directory.0.join("project.arcadia"); + save(&mut app, path.clone()).unwrap(); + send(&mut app, Message::SourceFile("project.arcadia".into())); + let removed = app + .source_editor + .text() + .replace(" include \"spare.arcadia\";\n", ""); + edit(&mut app, &removed); + assert!(app.apply_source()); + send(&mut app, Message::SourceCloseFile("spare.arcadia".into())); + assert!(!app.source_workspace.files.contains_key("spare.arcadia")); + assert!( + app.disk_project + .as_ref() + .unwrap() + .expectations + .contains_key("spare.arcadia") + ); + std::fs::write(directory.0.join("spare.arcadia"), "// external edit\n").unwrap(); + assert!(save(&mut app, path.clone()).is_err()); + assert!( + std::fs::read_to_string(&path) + .unwrap() + .contains("include \"spare.arcadia\"") + ); + std::fs::write(directory.0.join("spare.arcadia"), "").unwrap(); + save(&mut app, path.clone()).unwrap(); + assert!( + !app.disk_project + .as_ref() + .unwrap() + .expectations + .contains_key("spare.arcadia") + ); + std::fs::write(directory.0.join("spare.arcadia"), "// new external text\n").unwrap(); + let id = app.project().id; + send( + &mut app, + Message::Command(Command::Rename(id, "New project name".into())), + ); + save(&mut app, path).unwrap(); + let mut entry = app.source_editor.text(); + entry.insert_str( + app.document.compiled().index.project_body.span.end, + "\n include \"spare.arcadia\";\n", + ); + edit(&mut app, &entry); + let disk = source_files::discover_missing( + app.disk_project.as_ref().unwrap(), + &app.source_workspace.bundle().files, + LIMITS, + ) + .unwrap(); + let version = app.source_version; + send(&mut app, Message::IncludesLoaded(version, Ok(disk))); + assert_eq!( + app.source_workspace.files["spare.arcadia"].text, + "// new external text\n" + ); + assert!(app.apply_source()); + } + #[test] + fn json_export_and_rejected_save_as_preserve_source_location_history_and_bytes() { + let directory = Directory::new(); + let mut app = App::with_project(Project::empty()); + let id = app.project().id; + send( + &mut app, + Message::Command(Command::Rename(id, "Unsaved model".into())), + ); + let before = app.document.snapshot(); + let revision = app.document.revision(); + save(&mut app, directory.0.join("export.json")).unwrap(); + assert!(app.path.is_none()); + assert!(app.dirty()); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + let path = directory.0.join("collision.arcadia"); + std::fs::write(&path, "Preserve existing file").unwrap(); + assert!(save(&mut app, path.clone()).is_err()); + assert_eq!( + std::fs::read_to_string(path).unwrap(), + "Preserve existing file" + ); + assert_eq!(app.document.revision(), revision); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + } +} + +#[cfg(test)] +mod port_view_tests { + use super::*; + + #[test] + fn catalog_creation_follows_the_new_port_and_keeps_group_browsing_explicit() { + let mut project = Project::empty(); + let actor = project + .add_element(Kind::SystemActor, Layer::Sa, "External operator", None) + .unwrap(); + let mut app = App::with_project(project); + let _ = app.update(Message::Explore(Some(actor))); + app.explorer_page = 3; + app.inspector_tab = crate::navigation::InspectorTab::Connections; + let _ = app.update(Message::PortAction(crate::port_workflow::Action::Component)); + let _ = app.update(Message::Workflow(crate::workflow::Event::Apply)); + assert_eq!(app.page, Page::Catalog); + assert_eq!(app.project().elements[&app.selected].parent, Some(actor)); + assert_eq!(app.explorer_focus, Some(app.selected)); + assert_eq!(app.explorer_group, app.explorer_index.group(app.selected)); + assert_eq!(app.explorer_page, 0); + assert_eq!( + app.inspector_tab, + crate::navigation::InspectorTab::Properties + ); + let selected = app.selected; + let group = app.explorer_group.unwrap(); + let _ = app.update(Message::BrowseGroup(group)); + assert_eq!(app.selected, selected); + assert_eq!(app.explorer_group, Some(group)); + assert!(app.explorer_focus.is_none()); + let _ = app.update(Message::Page(Page::Guide)); + let _ = app.update(Message::Page(Page::Catalog)); + assert_eq!(app.explorer_focus, Some(selected)); + assert_eq!(app.explorer_page, 0); + } + + #[test] + fn filtered_active_view_cannot_hide_a_created_port_when_eligible_owner_view_is_folded() { + let mut p = Project::empty(); + let system = p + .add_element(Kind::System, Layer::Sa, "System", None) + .unwrap(); + let function = p + .add_element(Kind::Function, Layer::Sa, "Function", None) + .unwrap(); + p.relate(RelationKind::Allocation, "Performs", system, function); + let architecture = p.ensure_diagram(Layer::Sa); + p.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation; + let container = p.place(architecture, system, Position { x: 40., y: 40. }); + p.place(architecture, function, Position { x: 70., y: 130. }); + let capabilities = Id::new_v4(); + p.diagrams.insert( + capabilities, + Diagram { + id: capabilities, + alias: "filtered_view".into(), + name: "Filtered view".into(), + description: String::new(), + layer: Layer::Sa, + projection: ProjectionKind::Capabilities, + routes: Vec::new(), + view_options: Default::default(), + nodes: Default::default(), + }, + ); + p.place(capabilities, system, Position { x: 40., y: 40. }); + p.place(capabilities, function, Position { x: 70., y: 130. }); + p.diagrams + .get_mut(&architecture) + .unwrap() + .nodes + .get_mut(&container) + .unwrap() + .collapsed = true; + let mut app = App::with_project(p.clone()); + let _ = app.update(Message::Select(function)); + app.active_diagram = Some(capabilities); + app.refresh_draft(); + app.inspector_tab = crate::navigation::InspectorTab::Connections; + let _ = app.update(Message::PortAction(crate::port_workflow::Action::Output)); + let _ = app.update(Message::Workflow(crate::workflow::Event::Apply)); + assert!(app.workflow.is_none(), "{}", app.status); + assert_eq!( + app.inspector_tab, + crate::navigation::InspectorTab::Properties + ); + assert_eq!(app.active_diagram, Some(architecture)); + assert!( + project_view(app.project(), architecture) + .ports + .iter() + .any(|port| port.element == app.selected) + ); + assert!(!app.project().diagrams[&architecture].nodes[&container].collapsed); + let _ = app.update(Message::Undo); + assert_eq!( + app.project(), + &p, + "Creation and the eligible owner reveal remain one transaction" + ); + } +} + +#[cfg(test)] +mod lint_tests { + use super::*; + + fn edit(app: &mut App, value: &str) { + let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll)); + let _ = app.update(Message::SourceEdit(text_editor::Action::Edit( + text_editor::Edit::Paste(Arc::new(value.into())), + ))); + } + + #[test] + fn lint_coalesces_work_and_rejects_obsolete_results() { + let mut app = App::with_project(Project::empty()); + let _ = app.request_lint(); + let first = app.source_version; + edit(&mut app, "?"); + let latest = app.source_version; + let _ = app.update(Message::Lint(latest)); + assert_eq!(app.lint_running, Some(first)); + assert_eq!(app.lint_requested, Some(latest)); + + let errors = source_workspace::lint(&app.source_workspace.bundle()); + let _ = app.update(Message::Linted(first, Ok(errors.clone()))); + assert!(app.source_diagnostics.is_empty()); + assert_eq!(app.lint_running, Some(latest)); + let _ = app.update(Message::Linted(latest, Ok(errors.clone()))); + assert_eq!(app.source_diagnostics, errors); + assert_eq!(app.lint_running, None); + assert_eq!(app.source_editor.text(), "?"); + } + + #[test] + fn prior_document_lint_cannot_enter_reopened_model_or_block_next_checks() { + let mut app = App::with_project(Project::empty()); + edit(&mut app, "?"); + let _ = app.request_lint(); + let old_version = app.source_version; + app.reset(Project::empty(), None); + assert!(app.source_version > old_version); + let _ = app.request_lint(); + app.busy = true; + let _ = app.update(Message::Linted( + old_version, + Ok(source_workspace::lint(&app.source_workspace.bundle())), + )); + assert!(app.source_diagnostics.is_empty()); + assert_eq!(app.lint_running, Some(app.source_version)); + let _ = app.update(Message::Linted(app.source_version, Ok(vec![]))); + assert_eq!(app.lint_running, None); + } + + #[test] + fn committed_check_cache_follows_edit_and_undo_while_drafts_stay_separate() { + let mut app = App::with_project(Project::empty()); + let original = app.issue_count(); + let element = app + .project() + .make_element(Kind::Function, Layer::Sa, "Assess request", None) + .unwrap(); + let _ = app.update(Message::Command(Command::CreateElement(element))); + assert!( + app.model_issues + .iter() + .any(|issue| issue.code == "missing_system") + ); + assert_eq!(app.issue_count(), validate(app.project()).len()); + let after = app.issue_count(); + edit(&mut app, "?"); + assert_eq!(app.issue_count(), after); + let _ = app.update(Message::RevertSource); + let _ = app.update(Message::Undo); + assert_eq!(app.issue_count(), original); + assert!(app.project().elements.is_empty()); + } +} + +#[cfg(test)] +mod data_paging_lifecycle_tests { + use super::*; + #[test] + fn paging_preserves_same_context_and_resets_on_layer_or_document_replacement() { + let mut p = Project::empty(); + let mut oa = None; + for layer in [Layer::Oa, Layer::Sa] { + for index in 0..70 { + let id = p + .add_element( + Kind::Interface, + layer, + &format!("Interface {layer} {index}"), + None, + ) + .unwrap(); + if layer == Layer::Oa { + oa = Some(id); + } + } + } + let mut app = App::with_project(p.clone()); + let page = Message::Data(crate::data_view::Event::Page { + section: crate::data_view::Section::Catalog, + owner: None, + page: 2, + }); + let _ = app.update(Message::Page(Page::Data)); + let _ = app.update(page.clone()); + let retained = app.data_state.clone(); + let _ = app.update(Message::Page(Page::Catalog)); + let _ = app.update(Message::Page(Page::Data)); + let _ = app.update(Message::Layer(Layer::Oa)); + assert_eq!(app.data_state, retained); + let _ = app.update(Message::Layer(Layer::Sa)); + assert_eq!(app.data_state, crate::data_view::State::default()); + let _ = app.update(page.clone()); + let _ = app.update(Message::Select(oa.unwrap())); + assert_eq!(app.layer, Layer::Oa); + assert_eq!(app.data_state, crate::data_view::State::default()); + let _ = app.update(page); + app.reset(p, None); + assert_eq!(app.data_state, crate::data_view::State::default()); + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app/process_canvas.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app/process_canvas.rs new file mode 100644 index 00000000..4a48ce3c --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app/process_canvas.rs @@ -0,0 +1,715 @@ +use super::*; +use crate::behavior::topology::{self, CanvasEvent}; +impl App { + pub(crate) fn topology_context(&self) -> Option { + let owner = crate::behavior::selected_owner(self)?; + (self.page == Page::Behavior + && matches!( + self.project().elements.get(&owner)?.kind(), + Kind::Process | Kind::FunctionalChain + )) + .then_some(topology::Context { + document: self.canvas_document, + owner, + revision: self.document.revision(), + epoch: self.canvas_epoch, + }) + } + pub(super) fn reveal_process_selection(&mut self) -> Task { + if let Some(context) = self.topology_context() { + let mut state = std::mem::take(&mut self.behavior.topology); + if state.reveal_selection(self.project(), context.owner, self.selected) { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + self.behavior.topology = state; + } + Task::none() + } + pub(super) fn edit_process_canvas(&mut self, event: CanvasEvent) -> Task { + let context = event.context(); + if let CanvasEvent::Scrolled(_, area, offset) = event { + if self.topology_context().is_some_and(|c| { + c.document == context.document + && c.owner == context.owner + && c.revision == context.revision + }) && self.behavior.topology.scrolled(area, offset) + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + return Task::none(); + } + if let CanvasEvent::Viewport(_, size) = event { + if self.topology_context().is_some_and(|c| { + c.document == context.document + && c.owner == context.owner + && c.revision == context.revision + }) && self.behavior.topology.viewport(size) + { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + return Task::none(); + } + if let CanvasEvent::Camera(_, lifetime, input) = event { + use crate::behavior::canvas_workspace::{self, Event}; + if lifetime != self.camera_epoch + || !self.topology_context().is_some_and(|c| { + c.document == context.document + && c.owner == context.owner + && c.revision == context.revision + }) + { + return Task::none(); + } + let scope = canvas_workspace::Scope { + document: context.document, + owner: context.owner, + revision: context.revision, + epoch: lifetime, + }; + if self.behavior.has_pending() + || self.draft_dirty() + || self.source_dirty() + || !canvas_workspace::keyboard_enabled(self) + { + return Task::none(); + } + match input { + Event::Focus + | Event::Traverse(_) + | Event::ControlFocus(_) + | Event::ToggleFocus + | Event::Leave => { + return canvas_workspace::focus_task(scope, input); + } + Event::CancelGesture => self.canvas_epoch = self.canvas_epoch.wrapping_add(1), + input => { + if self.behavior.topology.camera(input) { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + } + return Task::none(); + } + if self.topology_context() != Some(context) { + return Task::none(); + } + self.behavior + .topology + .enter(context.document, context.owner); + match event { + CanvasEvent::ExpandQualified(_, endpoint) => { + let mut state = std::mem::take(&mut self.behavior.topology); + let changed = state.expand_context(self.project(), context.owner, endpoint); + self.behavior.topology = state; + if changed { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + CanvasEvent::OpenQualified(_, endpoint) => { + if self.behavior.has_pending() || !self.apply() { + return Task::none(); + } + let resolved = oa_core::topology_context::TopologyIndex::new(self.project()) + .resolve(context.owner, &endpoint); + if let Ok(resolved) = resolved { + self.behavior.topology.remember( + context.owner, + endpoint + .references + .first() + .copied() + .unwrap_or(endpoint.occurrence), + resolved.defining_sequence, + ); + self.behavior.active_owner = Some(resolved.defining_sequence); + self.select(endpoint.occurrence); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + CanvasEvent::SelectQualified(_, endpoint) => { + if !self.behavior.has_pending() + && !self.draft_dirty() + && !self.source_dirty() + && let Ok(resolved) = + oa_core::topology_context::TopologyIndex::new(self.project()) + .resolve(context.owner, &endpoint) + { + let reference = endpoint + .references + .first() + .copied() + .unwrap_or(endpoint.occurrence); + self.select(reference); + self.behavior.topology.select_context(resolved.address); + } + } + CanvasEvent::Focus(_) => { + self.behavior.topology.focused = !self.behavior.topology.focused; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + CanvasEvent::Select(_, id) => { + if !self.behavior.has_pending() + && crate::behavior::selected_behavior(self.project(), id) == Some(context.owner) + { + self.select(id); + self.selected_node = None; + self.behavior.topology.clear_context(); + } + } + CanvasEvent::Choose(_, tool) => { + if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() { + self.status = + "Apply or cancel pending edits before choosing a process tool.".into(); + return Task::none(); + } + self.behavior.topology.choose(tool); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + CanvasEvent::Zoom(_, zoom) => { + let mut state = std::mem::take(&mut self.behavior.topology); + state.camera_zoom(self.project(), context.owner, zoom); + self.behavior.topology = state; + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + CanvasEvent::Expand(_, id) => { + let mut state = std::mem::take(&mut self.behavior.topology); + let changed = state.expand(self.project(), context.owner, id); + self.behavior.topology = state; + if changed { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + CanvasEvent::Open(_, occurrence) => { + if self.behavior.has_pending() || !self.apply() { + return Task::none(); + } + if let Some(target) = + topology::referenced(self.project(), context.owner, occurrence) + { + if self + .project() + .elements + .get(&target) + .is_some_and(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain)) + { + self.behavior + .topology + .remember(context.owner, occurrence, target); + self.behavior.active_owner = Some(target); + } + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.select(target); + } + } + CanvasEvent::Back(_) => { + if self.behavior.has_pending() || !self.apply() { + return Task::none(); + } + let mut state = std::mem::take(&mut self.behavior.topology); + let back = state.back(self.project()); + self.behavior.topology = state; + if let Some((owner, occurrence)) = back { + self.behavior.active_owner = Some(owner); + self.select(occurrence); + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + } + } + mutation => { + if self.behavior.has_pending() || self.draft_dirty() || self.source_dirty() { + self.status = + "Apply or cancel pending edits before changing the process.".into(); + return Task::none(); + } + match mutation { + CanvasEvent::Begin { + tool, + control, + insertion, + .. + } => { + let mut state = std::mem::take(&mut self.behavior.topology); + let result = state.begin(self.project(), context, tool, control, insertion); + self.behavior.topology = state; + match result { + Ok(()) => { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.status="Choose the contextual contribution and apply at the captured position.".into(); + } + Err(e) => self.status = e, + } + } + CanvasEvent::AppendExchange(_) => { + let mut state = std::mem::take(&mut self.behavior.topology); + let result = state.append_exchange(self.project(), context.owner); + self.behavior.topology = state; + match result { + Ok(()) => { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.status = "Choose the next exchange; Create appends it with an explicit sequence link.".into(); + } + Err(e) => self.status = e, + } + } + CanvasEvent::Connect { source, target, .. } => { + let mut state = std::mem::take(&mut self.behavior.topology); + let result = state.connect(self.project(), context.owner, source, target); + self.behavior.topology = state; + match result { + Ok(()) => { + self.canvas_epoch = self.canvas_epoch.wrapping_add(1); + self.status = "Describe the path condition, then Create.".into(); + } + Err(e) => self.status = e, + } + } + CanvasEvent::Reconnect { + link, end, target, .. + } => { + match topology::reconnect(self.project(), context.owner, link, end, target) + { + Ok(command) => { + if self.execute(command) { + self.selected = link; + self.refresh_draft(); + return self.reveal_process_selection(); + } + } + Err(e) => self.status = e, + } + } + CanvasEvent::Reorder { + occurrence, before, .. + } => { + match topology::reorder(self.project(), context.owner, occurrence, before) { + Ok(command) => { + if self.execute(command) { + self.selected = occurrence; + self.refresh_draft(); + return self.reveal_process_selection(); + } + } + Err(e) => self.status = e, + } + } + _ => unreachable!(), + } + } + } + Task::none() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::behavior::{ + self, + topology::{ + CanvasTool, Event, Insertion, Tool, + direct_tests::{self, Fixture}, + }, + }; + fn setup() -> (App, Fixture) { + let f = direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(Page::Behavior)); + assert_eq!(app.topology_context().unwrap().owner, f.owner); + (app, f) + } + fn form(app: &mut App, event: Event) { + let _ = app.update(Message::Behavior(behavior::Event::Topology(event))); + } + fn begin(app: &App) -> CanvasEvent { + CanvasEvent::Begin { + context: app.topology_context().unwrap(), + tool: Tool::Work, + control: ControlKind::Choice, + insertion: Insertion::default(), + } + } + #[test] + fn process_focus_retains_rejected_draft_and_guards_selection_until_panels_restore() { + let (mut app, f) = setup(); + let original = app.document.snapshot(); + let initial = app.selected; + let event = begin(&app); + let _ = app.update(Message::TopologyCanvas(event)); + form( + &mut app, + Event::Name(" unfinished contextual work ".into()), + ); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context))); + assert!(app.behavior.topology.focused); + let raw = app.behavior.recovery_snapshot().unwrap(); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Select( + context, f.nodes[1], + ))); + assert_eq!(app.selected, initial); + form(&mut app, Event::Submit); + assert!(app.behavior.has_pending()); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert!(Arc::ptr_eq(&original, &app.document.snapshot())); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context))); + assert!(!app.behavior.topology.focused); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + } + #[test] + fn direct_process_append_empty_first_second_exchange_has_exact_source_history() { + for layer in [Layer::Oa, Layer::Sa] { + let (f, next) = direct_tests::empty_exchange_fixture(layer); + let mut app = App::with_project(f.p.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(Page::Behavior)); + let empty = app.document.snapshot(); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Begin { + context, + tool: Tool::Exchange, + control: ControlKind::Choice, + insertion: Insertion::default(), + })); + assert!(app.behavior.has_pending()); + form( + &mut app, + Event::Target(Choice { + id: f.exchange, + label: "Assessment".into(), + }), + ); + form(&mut app, Event::Name("First contribution".into())); + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending(), "{}", app.status); + let first_id = app.selected; + let first = app.document.snapshot(); + assert_eq!( + app.project().elements[&f.owner].steps().unwrap(), + &[first_id] + ); + assert_eq!(app.project().relations, empty.decoded.project.relations); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::AppendExchange( + context, + ))); + assert!( + app.behavior.has_pending(), + "reachable single-exchange continuation: {}", + app.status + ); + assert!(Arc::ptr_eq(&first, &app.document.snapshot())); + form( + &mut app, + Event::Target(Choice { + id: next, + label: "Response".into(), + }), + ); + form(&mut app, Event::Name("Second contribution".into())); + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending(), "{}", app.status); + let second_id = app.selected; + let second = app.document.snapshot(); + assert_ne!(first_id, second_id); + assert_eq!( + app.project().elements[&f.owner].steps().unwrap(), + &[first_id, second_id] + ); + let links = app + .project() + .relations + .values() + .filter(|r| r.kind() == RelationKind::SequenceLink) + .collect::>(); + assert_eq!(links.len(), 1); + assert_eq!( + (links[0].source(), links[0].target()), + (first_id, second_id) + ); + assert_eq!( + app.project().elements.len(), + empty.decoded.project.elements.len() + 2 + ); + for (id, relation) in &empty.decoded.project.relations { + assert_eq!(app.project().relations[id], *relation); + } + for work in f.work { + assert_eq!( + app.project().elements[&work], + empty.decoded.project.elements[&work] + ); + } + let reopened = Document::from_bundle( + second.bundle.clone(), + oa_core::source_project::ProjectLimits::default(), + ) + .unwrap(); + assert_eq!(reopened.project(), app.project()); + assert_eq!(reopened.bundle(), &second.bundle); + for expected in [&first, &empty] { + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &expected.bundle); + assert_eq!(app.project(), &expected.decoded.project); + } + assert!( + !app.document.can_undo(), + "each Create must add exactly one history entry" + ); + for expected in [&first, &second] { + let _ = app.update(Message::Redo); + assert_eq!(app.document.bundle(), &expected.bundle); + assert_eq!(app.project(), &expected.decoded.project); + } + assert!(!app.document.can_redo()); + } + } + #[test] + fn direct_process_app_stale_context_dirty_source_and_properties_never_mutate() { + let (mut app, _) = setup(); + let snapshot = app.document.snapshot(); + for field in 0..4 { + let mut event = begin(&app); + let CanvasEvent::Begin { context, .. } = &mut event else { + unreachable!() + }; + match field { + 0 => context.document = Id::new_v4(), + 1 => context.owner = Id::new_v4(), + 2 => context.revision += 1, + _ => context.epoch += 1, + }; + let _ = app.update(Message::TopologyCanvas(event)); + assert!(!app.behavior.has_pending()); + assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); + } + let _ = app.update(Message::Field("/name".into(), "Pending owner name".into())); + let event = begin(&app); + let _ = app.update(Message::TopologyCanvas(event)); + assert!(!app.behavior.has_pending()); + assert!(app.draft_dirty()); + let context = app.topology_context().unwrap(); + for event in [ + CanvasEvent::Zoom(context, Some(0.7)), + CanvasEvent::Viewport(context, iced::Size::new(510., 360.)), + ] { + let _ = app.update(Message::TopologyCanvas(event)); + assert!(app.draft_dirty()); + assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); + } + } + #[test] + fn direct_process_app_source_failure_retains_form_then_commits_once_and_selects_occurrence() { + let (mut app, f) = setup(); + let bundle = app.document.bundle().clone(); + let mut limits = oa_core::source_project::ProjectLimits::default(); + limits.model.max_source_bytes = + bundle.files.values().map(String::len).sum::() + 3000; + app.document = Document::from_bundle(bundle.clone(), limits).unwrap(); + app.sync(); + let event = begin(&app); + let _ = app.update(Message::TopologyCanvas(event)); + form( + &mut app, + Event::Target(Choice { + id: f.work[0], + label: "Assess".into(), + }), + ); + form(&mut app, Event::Name("Retain this long input".repeat(1000))); + let _ = app.update(Message::Page(Page::Catalog)); + assert_eq!(app.page, Page::Behavior); + form(&mut app, Event::Submit); + assert!(app.behavior.has_pending()); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo()); + form(&mut app, Event::Name("Recovered contribution".into())); + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending()); + assert_ne!(app.selected, f.owner); + assert_eq!(app.project().named(app.selected), "Recovered contribution"); + let created = app.selected; + let after = app.document.bundle().clone(); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &bundle); + let _ = app.update(Message::Redo); + assert_eq!(app.document.bundle(), &after); + assert!(app.project().contains(created)); + } + #[test] + fn direct_process_app_same_owner_selection_preserves_tool_but_owner_change_resets_it() { + let (mut app, f) = setup(); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Choose( + context, + CanvasTool::Connect, + ))); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Select( + context, f.nodes[0], + ))); + assert_eq!(app.topology_context(), Some(context)); + let other = app + .project() + .make_element(Kind::Process, Layer::Oa, "Other", None) + .unwrap(); + let other_id = other.id; + assert!(app.execute(Command::CreateElement(other))); + let _ = app.update(Message::BehaviorOwner(Choice { + id: other_id, + label: "Other".into(), + })); + assert_eq!(app.topology_context().unwrap().owner, other_id); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Connect { + context, + source: f.nodes[0], + target: f.nodes[2], + })); + assert!(!app.behavior.has_pending()); + } + #[test] + fn direct_process_app_save_scroll_resize_and_passive_geometry_keep_drafts_and_current_origin() { + let (mut app, f) = setup(); + let context = app.topology_context().unwrap(); + let original = app.document.snapshot(); + app.busy = true; + for (area, offset) in [ + ( + behavior::scenario::ScrollArea::Canvas, + iced::Vector::new(25., 100.), + ), + ( + behavior::scenario::ScrollArea::Page, + iced::Vector::new(0., 40.), + ), + ] { + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled( + context, area, offset, + ))); + } + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Viewport( + context, + iced::Size::new(500., 300.), + ))); + assert_eq!( + app.behavior.topology.scroll_offset(), + iced::Vector::new(25., 140.) + ); + assert_ne!(app.topology_context(), Some(context)); + assert!(app.busy); + let mut stale = context; + stale.document = Id::new_v4(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled( + stale, + behavior::scenario::ScrollArea::Canvas, + iced::Vector::new(0., 900.), + ))); + assert_eq!( + app.behavior.topology.scroll_offset(), + iced::Vector::new(25., 140.) + ); + let root = PathBuf::from("process-scroll-save-fixture"); + let receipt = FileSnapshot { + document: context.document, + revision: context.revision, + snapshot: original.clone(), + path: root.join(&original.bundle.entry), + disk: Some(source_files::DiskProject { + root, + bundle: original.bundle.clone(), + expectations: Default::default(), + reachable: original.bundle.files.keys().cloned().collect(), + diagnostics: vec![], + }), + }; + let _ = app.update(Message::Saved(Ok(Some(receipt)))); + assert!(!app.busy); + assert!(!app.document.can_undo()); + assert_eq!(app.project(), &f.p); + let context = app.topology_context().unwrap(); + let size = app.window_size; + let _ = app.update(Message::WindowResized(size)); + assert_eq!(app.topology_context(), Some(context)); + let _ = app.update(Message::WindowResized(iced::Size::new( + size.width + 20., + size.height, + ))); + assert_ne!(app.topology_context(), Some(context)); + let event = begin(&app); + let _ = app.update(Message::TopologyCanvas(event)); + form(&mut app, Event::Name("Keep this form".into())); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Scrolled( + context, + behavior::scenario::ScrollArea::Page, + iced::Vector::new(0., 80.), + ))); + assert!(app.behavior.has_pending()); + assert_eq!(app.document.bundle(), &original.bundle); + } + #[test] + fn direct_process_app_open_source_and_back_preserve_local_reference_identity() { + let (mut app, f) = setup(); + let source = app + .project() + .make_element(Kind::Process, Layer::Oa, "Reusable source", None) + .unwrap(); + let source_id = source.id; + assert!(app.execute(Command::CreateElement(source))); + let mut occurrence = app + .project() + .make_element(Kind::Occurrence, Layer::Oa, "Local use", Some(f.owner)) + .unwrap(); + let occurrence_id = occurrence.id; + if let Some(SharedBody::Occurrence { body, .. }) = occurrence.shared_mut() { + *body = OccurrenceBody::SequenceReference { + sequence: Some(source_id), + }; + } + assert!(app.execute(Command::CreateElement(occurrence))); + let snapshot = app.document.snapshot(); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Open( + context, + occurrence_id, + ))); + assert_eq!(app.selected, source_id); + assert_eq!(app.topology_context().unwrap().owner, source_id); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Back(context))); + assert_eq!(app.selected, occurrence_id); + assert_eq!(app.topology_context().unwrap().owner, f.owner); + assert!(Arc::ptr_eq(&snapshot, &app.document.snapshot())); + } +} + +#[cfg(test)] +#[test] +fn direct_process_dirty_source_blocks_canvas_creation_without_losing_text() { + let f = topology::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(Page::Behavior)); + let original = app.document.snapshot(); + let edited = format!( + "{}\n// Retained source draft\n", + app.source_workspace.text() + ); + let _ = app.update(Message::SourceEdit(text_editor::Action::SelectAll)); + let _ = app.update(Message::SourceEdit(text_editor::Action::Edit( + text_editor::Edit::Paste(edited.clone().into()), + ))); + assert!(app.source_dirty()); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Begin { + context, + tool: topology::Tool::Work, + control: ControlKind::Choice, + insertion: topology::Insertion::default(), + })); + assert!(!app.behavior.has_pending()); + assert!(app.source_dirty()); + assert_eq!(app.source_workspace.text(), edited); + assert!(Arc::ptr_eq(&original, &app.document.snapshot())); +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app/reference_authoring.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app/reference_authoring.rs new file mode 100644 index 00000000..c8820787 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/app/reference_authoring.rs @@ -0,0 +1,304 @@ +//! One transient chooser for the currently mounted Inspector or review field. +//! Browsing never calls a draft setter or semantic planner. +use super::{App, Message, Page, Pending}; +use crate::{ + forms::{ + Draft, + reference_chooser::{self as chooser, Action, Close, Outcome, adapter, trigger}, + }, + navigation::InspectorTab, + semantic_review::{self, RowAddress, RowEvent}, + workflow, +}; +use iced::{Element, Rectangle, Task}; +use oa_core::{FacetSelector, Id, Layer}; + +#[derive(Debug, Clone, PartialEq, Eq)] +enum Host { + Inspector, + Review { + address: Option, + facet: FacetSelector, + }, +} +impl Host { + fn message(&self, event: chooser::Event) -> Message { + match self { + Self::Inspector => Message::FieldReferenceEvent(event), + Self::Review { address, facet } => { + Message::Workflow(workflow::Event::semantic(semantic_review::Event::Row { + address: *address, + facet: Some(facet.clone()), + event: RowEvent::ReferenceEvent(event), + })) + } + } + } +} +struct Opened { + host: Host, + request: adapter::Open, + document_session: Id, + page: Page, + layer: Layer, + chooser: chooser::State, +} +#[derive(Default)] +pub(super) struct State { + opened: Option, +} + +impl App { + pub(crate) fn reference_input_active(&self) -> bool { + self.reference_authoring.opened.is_some() + } + + fn reference_draft(&self, host: &Host, path: &str) -> Option<&Draft> { + match host { + Host::Inspector + if self.workflow.is_none() && self.inspector_tab == InspectorTab::Properties => + { + self.draft.as_ref() + } + Host::Review { address, facet } => match self.workflow.as_ref()? { + workflow::Workflow::Semantic(state) => { + state.reference_draft(self.project(), *address, facet, path) + } + _ => None, + }, + _ => None, + } + } + fn reference_available(&self) -> bool { + !self.busy + && !self.hosted.visible + && self.architecture.is_none() + && self.model_menu.is_none() + && self.file_prompt.is_none() + && !self.delete_pending + && self.pending.is_none() + && !self.source_dirty() + && !self.behavior.has_pending() + && !matches!(self.page, Page::Home | Page::Guide | Page::Text) + } + fn reference_valid(&self, opened: &Opened) -> bool { + self.reference_available() + && self.canvas_document == opened.document_session + && self.page == opened.page + && self.layer == opened.layer + && self.document.revision() == opened.chooser.stamp().context.revision + && self.project().id == opened.request.document + && self + .reference_draft(&opened.host, &opened.request.path) + .is_some_and(|draft| draft.reference_matches(&opened.request)) + } + pub(super) fn reconcile_reference_chooser(&mut self) { + if self + .reference_authoring + .opened + .as_ref() + .is_some_and(|opened| !self.reference_valid(opened)) + { + self.reference_authoring + .opened + .as_mut() + .unwrap() + .chooser + .invalidate(); + self.reference_authoring.opened = None; + } + } + fn open_reference(&mut self, host: Host, request: adapter::Open) -> Task { + if !self.reference_available() { + return Task::none(); + } + let Some(draft) = self.reference_draft(&host, &request.path) else { + return Task::none(); + }; + let Ok(chooser) = + draft.reference_catalog(self.project(), &request, self.document.revision()) + else { + return Task::none(); + }; + self.reference_authoring.opened = Some(Opened { + host, + request, + document_session: self.canvas_document, + page: self.page, + layer: self.layer, + chooser, + }); + Task::none() + } + fn reference_event(&mut self, host: Host, event: chooser::Event) -> Task { + let Some(mut opened) = self.reference_authoring.opened.take() else { + return Task::none(); + }; + if !self.reference_valid(&opened) { + return Task::none(); + } + if opened.host != host { + self.reference_authoring.opened = Some(opened); + return Task::none(); + } + match opened.chooser.update(event) { + Outcome::Ignored | Outcome::Browsed => { + self.reference_authoring.opened = Some(opened); + Task::none() + } + Outcome::Closed(reason) => self.reference_focus( + &opened, + match reason { + Close::Escape => Some(0), + Close::Tab { backwards } => Some(if backwards { -1 } else { 1 }), + Close::Outside | Close::WindowChanged => None, + }, + ), + Outcome::Write { context, value } => { + let intent = adapter::Set { + context, + generation: opened.request.generation, + value, + }; + let result = match &opened.host { + Host::Inspector => self + .draft + .as_mut() + .ok_or_else(|| "The property editor closed.".to_owned()) + .and_then(|draft| draft.reference_set(self.document.project(), &intent)), + Host::Review { address, facet } => match self.workflow.as_mut() { + Some(workflow::Workflow::Semantic(state)) => { + state.set_reference(self.document.project(), *address, facet, intent) + } + _ => Err("The review editor closed.".into()), + }, + }; + if let Err(error) = result { + self.status = error; + } + // A scoped write rotates field generation. The task targets the + // newly rendered field, while a later structural edit makes it inert. + self.reference_focus(&opened, Some(0)) + } + } + } + fn reference_focus(&self, opened: &Opened, step: Option) -> Task { + let Some(step) = step else { + return Task::none(); + }; + let Some(draft) = self.reference_draft(&opened.host, &opened.request.path) else { + return Task::none(); + }; + if draft.reference_session() != opened.request.session { + return Task::none(); + } + trigger::focus( + draft.reference_session(), + draft.reference_generation(), + &opened.request.path, + step, + ) + } + /// Called before generic modal dispatch: the review owns a modal, and this + /// chooser owns only its active field. All browser events stop here. + pub(super) fn reference_message(&mut self, message: &Message) -> Option> { + self.reconcile_reference_chooser(); + match message { + Message::FieldReferenceOpen(request) => { + Some(self.open_reference(Host::Inspector, request.clone())) + } + Message::FieldReferenceEvent(event) => { + Some(self.reference_event(Host::Inspector, event.clone())) + } + Message::Workflow(workflow::Event::Semantic(review_event)) => { + let semantic_review::Event::Row { + address, + facet: Some(facet), + event, + } = review_event.as_ref() + else { + return None; + }; + let host = Host::Review { + address: *address, + facet: facet.clone(), + }; + match event { + RowEvent::ReferenceOpen(request) => { + Some(self.open_reference(host, request.clone())) + } + RowEvent::ReferenceEvent(event) => { + Some(self.reference_event(host, event.clone())) + } + _ => None, + } + } + Message::Escape | Message::Request(Pending::Close(_)) + if self.reference_authoring.opened.is_some() => + { + let opened = self.reference_authoring.opened.as_ref().unwrap(); + let host = opened.host.clone(); + let event = opened.chooser.event(Action::Close(Close::Escape)); + Some(self.reference_event(host, event)) + } + // Global shortcuts are also emitted for keys captured by the search + // input. They must not apply drafts, navigate, or undo the model. + Message::Save(_) + | Message::ShortcutUndo(_) + | Message::Request(Pending::Open | Pending::New) + | Message::SourceNavigation(_) + | Message::Page(Page::Guide) + if self.reference_authoring.opened.is_some() => + { + Some(Task::none()) + } + _ => None, + } + } + pub(super) fn reference_overlay< + 'a, + R: iced::advanced::text::Renderer + 'a, + >( + &'a self, + content: Element<'a, Message, iced::Theme, R>, + ) -> Element<'a, Message, iced::Theme, R> { + let opened = self + .reference_authoring + .opened + .as_ref() + .filter(|opened| self.reference_valid(opened)); + let host = opened + .map(|opened| opened.host.clone()) + .unwrap_or(Host::Inspector); + let focus_scope = if self.reference_available() { + match self.workflow.as_ref() { + None if self.inspector_tab == InspectorTab::Properties => { + self.draft.as_ref().map(|draft| trigger::FormScope { + session: draft.reference_session(), + modal: false, + }) + } + Some(workflow::Workflow::Semantic(state)) => state + .reference_form(self.project()) + .map(|draft| trigger::FormScope { + session: draft.reference_session(), + modal: true, + }), + _ => None, + } + } else { + None + }; + chooser::widget::Popup::new( + content, + opened.map(|opened| &opened.chooser), + opened.map_or(Rectangle::default(), |opened| opened.request.anchor), + move |event| host.message(event), + ) + .focus_scope(focus_scope) + .into() + } +} + +#[cfg(test)] +mod tests; diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior.rs new file mode 100644 index 00000000..54e18056 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior.rs @@ -0,0 +1,3367 @@ +//! Readable behavioral projections. Selection edits the same typed records as text. +use crate::{ + app::{App, Choice, Message}, + style, + views::{action, heading, label, short}, +}; +use iced::{ + Color, Element, Fill, Point, Rectangle, Renderer, Size, Theme, mouse, + widget::{ + Action, button, + canvas::{self, Frame, Path, Stroke, Text}, + column, container, pick_list, row, scrollable, text, + }, +}; +use oa_core::*; +use std::collections::BTreeMap; +pub(crate) mod canvas_workspace; +#[cfg(test)] +mod contextual_source_tests; +pub(crate) mod creation; +mod form_session; +pub(crate) mod scenario; +// Projection acceptance precedes the coordinated scenario renderer integration. +#[cfg(test)] +mod scenario_lifecycle; +pub(crate) mod source_draft; +pub(crate) mod state_canvas; +pub(crate) mod state_edit; +pub(crate) mod state_menu; +pub(crate) mod state_view; +pub(crate) mod states; +pub(crate) mod topology; +// The standard on_scroll callback only reports active scrolling. This local +// wrapper reads the existing Scrollable's actual translation as well, including +// initial mount and passive size clamping. It forwards layout, input and drawing. +mod scenario_scroll { + use super::*; + use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, renderer, widget}; + use iced::{Length, Vector, overlay}; + + pub(super) fn observe<'a, R: renderer::Renderer + 'a>( + context: scenario::Context, + area: scenario::ScrollArea, + process: bool, + restore: Option, + content: impl Into>, + ) -> Element<'a, Message, Theme, R> { + Element::new(Observer { + content: content.into(), + context, + area, + process, + restore, + }) + } + struct Observer<'a, R = Renderer> { + content: Element<'a, Message, Theme, R>, + context: scenario::Context, + area: scenario::ScrollArea, + process: bool, + restore: Option, + } + type Report = Option<(bool, Id, Id, Vector)>; + struct ObservedScroll { + identity: (Id, Id), + report: Report, + restored: Option, + } + #[derive(Default)] + struct Translation(Option); + impl widget::Operation for Translation { + fn traverse(&mut self, _: &mut dyn FnMut(&mut dyn widget::Operation)) {} + fn scrollable( + &mut self, + _: Option<&widget::Id>, + _: Rectangle, + _: Rectangle, + translation: Vector, + _: &mut dyn widget::operation::Scrollable, + ) { + self.0 = Some(translation); + } + } + struct RestoreTranslation { + offset: Vector, + applied: bool, + } + impl widget::Operation for RestoreTranslation { + // Restore only this wrapper's root Scrollable, never its nested canvas. + fn traverse(&mut self, _: &mut dyn FnMut(&mut dyn widget::Operation)) {} + fn scrollable( + &mut self, + _: Option<&widget::Id>, + _: Rectangle, + _: Rectangle, + _: Vector, + state: &mut dyn widget::operation::Scrollable, + ) { + state.scroll_to(widget::operation::scrollable::AbsoluteOffset { + x: Some(self.offset.x), + y: Some(self.offset.y), + }); + self.applied = true; + } + } + impl Observer<'_, R> { + fn report( + &mut self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &R, + shell: &mut Shell<'_, Message>, + ) { + let restore = self.restore.filter(|request| { + !self.process + && (request.document, request.owner) + == (self.context.document, self.context.scenario) + && tree.state.downcast_ref::().restored != Some(request.token) + }); + let receipt = restore.and_then(|request| { + let mut operation = RestoreTranslation { + offset: request.offset, + applied: false, + }; + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + &mut operation, + ); + operation.applied.then_some(request.token) + }); + let mut offset = Translation::default(); + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + &mut offset, + ); + if let Some(offset) = offset.0 { + let next = Some(( + self.process, + self.context.document, + self.context.scenario, + offset, + )); + let state = tree.state.downcast_mut::(); + if let Some(token) = receipt { + state.restored = Some(token); + state.report = next; + shell.publish(Message::ScenarioCanvas(scenario::CanvasEvent::Restored( + self.context, + token, + self.area, + offset, + ))); + return; + } + let previous = &mut state.report; + if *previous != next { + *previous = next; + shell.publish(if self.process { + Message::TopologyCanvas(topology::CanvasEvent::Scrolled( + topology::Context { + document: self.context.document, + owner: self.context.scenario, + revision: self.context.revision, + epoch: self.context.epoch, + }, + self.area, + offset, + )) + } else { + Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled( + self.context, + self.area, + offset, + )) + }); + } + } + } + } + impl Widget for Observer<'_, R> { + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + fn state(&self) -> widget::tree::State { + widget::tree::State::new(ObservedScroll { + identity: (self.context.document, self.context.scenario), + report: None, + restored: None, + }) + } + fn children(&self) -> Vec { + vec![widget::Tree::new(self.content.as_widget())] + } + fn diff(&self, tree: &mut widget::Tree) { + if tree.state.downcast_ref::().identity + != (self.context.document, self.context.scenario) + { + // A new owner/document needs fresh real scroll state, not merely + // cleared mirrored offsets. Edits and tool/form changes retain it. + tree.state = self.state(); + tree.children = self.children(); + } else { + tree.diff_children(std::slice::from_ref(&self.content)); + } + } + fn size(&self) -> Size { + self.content.as_widget().size() + } + fn size_hint(&self) -> Size { + self.content.as_widget().size_hint() + } + fn layout( + &mut self, + tree: &mut widget::Tree, + renderer: &R, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + } + fn update( + &mut self, + tree: &mut widget::Tree, + event: &iced::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &R, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + // A passive clamp notification precedes any pointer result produced + // with the old context; App rejects that result after the epoch bump. + self.report(tree, layout, renderer, shell); + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + self.report(tree, layout, renderer, shell); + } + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut R, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + fn operate( + &mut self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &R, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + fn mouse_interaction( + &self, + tree: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &R, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'b>, + renderer: &R, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } + } + // Iced exposes its null renderer only in debug builds. + #[cfg(all(test, debug_assertions))] + mod tests { + use super::*; + fn observer(height: f32, context: scenario::Context) -> Observer<'static, ()> { + Observer { + context, + area: scenario::ScrollArea::Page, + process: false, + restore: None, + content: scrollable(iced::widget::Space::new().width(200.).height(height)) + .id("observer-test") + .width(200.) + .height(200.) + .into(), + } + } + fn update( + observer: &mut Observer<'_, ()>, + tree: &mut widget::Tree, + event: iced::Event, + ) -> (Vec, bool) { + let node = observer.layout( + tree, + &(), + &layout::Limits::new(Size::ZERO, Size::new(800., 700.)), + ); + let mut messages = vec![]; + let mut shell = Shell::new(&mut messages); + observer.update( + tree, + &event, + Layout::new(&node), + mouse::Cursor::Available(Point::new(50., 50.)), + &(), + &mut iced::advanced::clipboard::Null, + &mut shell, + &Rectangle::with_size(Size::new(800., 700.)), + ); + let captured = shell.is_event_captured(); + (messages, captured) + } + fn offsets(messages: &[Message]) -> Vec { + messages + .iter() + .filter_map(|message| { + if let Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled(_, _, offset)) = + message + { + Some(*offset) + } else { + None + } + }) + .collect() + } + fn actual_offset(view: &mut Observer<'_, ()>, tree: &mut widget::Tree) -> Vector { + let node = view.layout( + tree, + &(), + &layout::Limits::new(Size::ZERO, Size::new(800., 700.)), + ); + let mut operation = Translation::default(); + view.operate(tree, Layout::new(&node), &(), &mut operation); + operation.0.unwrap() + } + #[test] + fn direct_scenario_caller_restores_mounted_scroll_once_with_clamping_and_app_receipt() { + use scenario::{ + CanvasEvent, ScrollArea, + direct_tests::{navigation_event, navigation_fixture}, + }; + let redraw = || { + iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )) + }; + for area in [ScrollArea::Page] { + for returned_height in [600., 250.] { + let (project, [a, _, _], [use_id, _, _]) = navigation_fixture(); + let mut app = App::with_project(project.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let source = app.document.bundle().clone(); + let mut view = observer(600., app.scenario_context().unwrap()); + view.area = area; + let mut tree = widget::Tree::new(&view as &dyn Widget); + for message in update(&mut view, &mut tree, redraw()).0 { + let _ = app.update(message); + } + for message in update( + &mut view, + &mut tree, + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. }, + }), + ) + .0 + { + let _ = app.update(message); + } + assert_eq!(actual_offset(&mut view, &mut tree), Vector::new(0., 180.)); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, use_id)); + view = observer(600., app.scenario_context().unwrap()); + view.area = area; + view.diff(&mut tree); + for message in update(&mut view, &mut tree, redraw()).0 { + let _ = app.update(message); + } + assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO); + navigation_event(&mut app, CanvasEvent::Back); + assert_eq!(app.selected, use_id); + let context = app.scenario_context().unwrap(); + let request = app.behavior.scenario.scroll_restore(context, area).unwrap(); + view = observer(returned_height, context); + view.area = area; + view.restore = Some(request); + view.diff(&mut tree); + let expected = Vector::new(0., (returned_height - 200.).min(180.)); + let messages = update(&mut view, &mut tree, redraw()).0; + assert!( + matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Restored(_, _, _, actual))] if *actual == expected) + ); + assert_eq!(actual_offset(&mut view, &mut tree), expected); + for message in messages { + let _ = app.update(message); + } + assert!( + app.behavior + .scenario + .scroll_restore(app.scenario_context().unwrap(), area) + .is_none() + ); + // A delayed receipt leaves the same request in the view; later input must not snap back. + for message in update( + &mut view, + &mut tree, + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { x: 0., y: 20. }, + }), + ) + .0 + { + let _ = app.update(message); + } + let moved = expected - Vector::new(0., 20.); + assert_eq!(actual_offset(&mut view, &mut tree), moved); + view.context = app.scenario_context().unwrap(); + view.diff(&mut tree); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + assert_eq!(actual_offset(&mut view, &mut tree), moved); + navigation_event(&mut app, |ctx| { + CanvasEvent::Restored(ctx, request.token, area, Vector::new(0., 999.)) + }); + let late_context = app.scenario_context().unwrap(); + let _ = app.update(Message::Page(crate::app::Page::Diagram)); + let epoch = app.canvas_epoch; + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Restored( + late_context, + request.token, + area, + Vector::ZERO, + ))); + assert_eq!(app.canvas_epoch, epoch); + assert_eq!(app.project(), &project); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo()); + } + } + } + #[test] + fn direct_scenario_restore_cannot_scroll_another_document_owner_or_process_view() { + for mismatch in 0..3 { + let context = scenario::direct_tests::context(Id::new_v4()); + let mut request = scenario::ScrollRestore { + document: context.document, + owner: context.scenario, + token: Id::new_v4(), + offset: Vector::new(0., 180.), + }; + match mismatch { + 0 => request.document = Id::new_v4(), + 1 => request.owner = Id::new_v4(), + _ => {} + } + let mut view = observer(600., context); + view.process = mismatch == 2; + view.restore = Some(request); + let mut tree = widget::Tree::new(&view as &dyn Widget); + let messages = update( + &mut view, + &mut tree, + iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )), + ) + .0; + assert_eq!(actual_offset(&mut view, &mut tree), Vector::ZERO); + assert!(!messages.iter().any(|m| matches!( + m, + Message::ScenarioCanvas(scenario::CanvasEvent::Restored(..)) + ))); + } + } + #[test] + fn independent_process_owner_change_resets_actual_scroll_offsets() { + let context = scenario::direct_tests::context(Id::new_v4()); + let redraw = || { + iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )) + }; + let process_offsets = |messages: Vec| { + messages + .into_iter() + .filter_map(|message| { + if let Message::TopologyCanvas(topology::CanvasEvent::Scrolled( + _, + _, + offset, + )) = message + { + Some(offset) + } else { + None + } + }) + .collect::>() + }; + let mut view = observer(600., context); + view.process = true; + let mut tree = widget::Tree::new(&view as &dyn Widget); + assert_eq!( + process_offsets(update(&mut view, &mut tree, redraw()).0), + vec![Vector::ZERO] + ); + let (messages, captured) = update( + &mut view, + &mut tree, + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. }, + }), + ); + assert!(captured); + assert_eq!(process_offsets(messages), vec![Vector::new(0., 180.)]); + let mut other = context; + other.scenario = Id::new_v4(); + let mut state = topology::State::default(); + state.enter(context.document, context.scenario); + state.scrolled(scenario::ScrollArea::Canvas, Vector::new(0., 180.)); + state.enter(other.document, other.scenario); + assert_eq!(state.scroll_offset(), Vector::ZERO); + view = observer(600., other); + view.process = true; + view.diff(&mut tree); + let reported = process_offsets(update(&mut view, &mut tree, redraw()).0); + assert_eq!( + reported, + vec![Vector::ZERO], + "The new process immediately reacquires its predecessor's scroll offset" + ); + } + + #[test] + fn direct_scroll_mounted_identity_resets_navigation_but_preserves_edits_before_first_report() + { + use widget::operation::scrollable::{AbsoluteOffset, scroll_to}; + for process in [false, true] { + for area in [scenario::ScrollArea::Canvas, scenario::ScrollArea::Page] { + for change in 0..4 { + let context = scenario::direct_tests::context(Id::new_v4()); + let mut view = observer(600., context); + view.process = process; + view.area = area; + let mut tree = widget::Tree::new(&view as &dyn Widget); + let node = view.layout( + &mut tree, + &(), + &layout::Limits::new(Size::ZERO, Size::new(800., 700.)), + ); + // Model a programmatic reveal before the observer has reported. + // Mounted identity must not depend on its last scroll notification. + let mut operation = scroll_to::<()>( + widget::Id::new("observer-test"), + AbsoluteOffset { + x: None, + y: Some(180.), + }, + ); + view.operate(&mut tree, Layout::new(&node), &(), &mut operation); + let mut next = context; + match change { + 0 => next.scenario = Id::new_v4(), + 1 => next.document = Id::new_v4(), + 2 => { + next.revision += 1; + next.epoch += 5; + } + _ => {} + } + view = observer(if change == 3 { 800. } else { 600. }, next); + view.process = process; + view.area = area; + view.diff(&mut tree); + let redraw = || { + iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )) + }; + let (messages, _) = update(&mut view, &mut tree, redraw()); + let actual = messages + .into_iter() + .filter_map(|message| match message { + Message::TopologyCanvas(topology::CanvasEvent::Scrolled( + c, + reported_area, + offset, + )) => { + assert_eq!( + (c.document, c.owner, c.revision, c.epoch), + (next.document, next.scenario, next.revision, next.epoch) + ); + assert_eq!( + std::mem::discriminant(&reported_area), + std::mem::discriminant(&area) + ); + Some(offset) + } + Message::ScenarioCanvas(scenario::CanvasEvent::Scrolled( + c, + reported_area, + offset, + )) => { + assert_eq!(c, next); + assert_eq!( + std::mem::discriminant(&reported_area), + std::mem::discriminant(&area) + ); + Some(offset) + } + _ => None, + }) + .collect::>(); + assert_eq!( + actual, + vec![if change < 2 { + Vector::ZERO + } else { + Vector::new(0., 180.) + }], + "process={process} area={area:?} change={change}" + ); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + } + } + } + } + + #[test] + fn direct_scroll_observer_reports_actual_initial_clamped_and_remounted_offsets_once() { + let context = scenario::direct_tests::context(Id::new_v4()); + let redraw = || { + iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )) + }; + let mut view = observer(600., context); + let mut tree = widget::Tree::new(&view as &dyn Widget); + assert_eq!( + offsets(&update(&mut view, &mut tree, redraw()).0), + vec![Vector::ZERO] + ); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + let (messages, captured) = update( + &mut view, + &mut tree, + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Pixels { x: 0., y: -180. }, + }), + ); + assert!(captured, "the observer forwards normal Scrollable capture"); + assert_eq!(offsets(&messages), vec![Vector::new(0., 180.)]); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + view = observer(250., context); + view.diff(&mut tree); + assert_eq!( + offsets(&update(&mut view, &mut tree, redraw()).0), + vec![Vector::new(0., 50.)] + ); + assert!(update(&mut view, &mut tree, redraw()).0.is_empty()); + view = observer(600., context); + view.diff(&mut tree); + assert_eq!( + offsets(&update(&mut view, &mut tree, redraw()).0), + vec![Vector::new(0., 180.)] + ); + let mut remounted = widget::Tree::new(&view as &dyn Widget); + assert_eq!( + offsets(&update(&mut view, &mut remounted, redraw()).0), + vec![Vector::ZERO] + ); + assert!(update(&mut view, &mut remounted, redraw()).0.is_empty()); + } + } +} +#[derive(Debug, Clone)] +pub enum Event { + Creation(creation::Event), + Topology(topology::Event), + Scenario(scenario::Event), + States(states::Event), + Submit, +} +impl Event { + pub fn is_source_back(&self) -> bool { + matches!( + self, + Self::Creation(creation::Event::Source(source_draft::Event::Edit( + _, + source_draft::Edit::Back + ))) | Self::Scenario(scenario::Event::Source(source_draft::Event::Edit( + _, + source_draft::Edit::Back + ))) | Self::Topology(topology::Event::Source(source_draft::Event::Edit( + _, + source_draft::Edit::Back + ))) + ) + } + pub fn is_source(&self) -> bool { + matches!( + self, + Self::Creation(creation::Event::Source(_)) + | Self::Scenario(scenario::Event::Source(_)) + | Self::Topology(topology::Event::Source(_)) + ) + } + pub fn starts_form(&self) -> bool { + matches!( + self, + Self::Creation(creation::Event::Open(..)) + | Self::Topology(topology::Event::Start(..)) + | Self::Scenario(scenario::Event::Start(..)) + | Self::States(states::Event::Start(..) | states::Event::Edit(..)) + ) + } + pub fn is_submit(&self) -> bool { + matches!( + self, + Self::Submit + | Self::Creation(creation::Event::Submit) + | Self::Topology(topology::Event::Submit) + | Self::Scenario(scenario::Event::Submit) + | Self::States(states::Event::Submit) + ) + } +} +#[derive(Default)] +pub struct State { + pub(crate) creation: creation::State, + pub active_owner: Option, + pub diagram: state_view::State, + pub(crate) topology: topology::State, + pub(crate) scenario: scenario::State, + states: states::State, +} +const PENDING_DRAFT: &str = "Apply or cancel the current behavior form before starting another."; +#[derive(serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct RecoveryDraft { + version: u32, + kind: RecoveryKind, + draft: serde_json::Value, +} +#[derive(serde::Serialize, serde::Deserialize)] +enum RecoveryKind { + Creation, + State, + Scenario, + Process, +} +impl State { + /// Capture only the pending authored form. Pointer gestures, cached choices, + /// viewport geometry and document/revision tokens never cross a restart. + pub fn recovery_snapshot(&self) -> Result, String> { + let drafts = [ + (RecoveryKind::Creation, self.creation.recovery_snapshot()?), + (RecoveryKind::State, self.states.recovery_snapshot()?), + (RecoveryKind::Scenario, self.scenario.recovery_snapshot()?), + (RecoveryKind::Process, self.topology.recovery_snapshot()?), + ]; + let mut drafts = drafts.into_iter().filter_map(|(kind, draft)| { + draft.map(|draft| RecoveryDraft { + version: 1, + kind, + draft, + }) + }); + let first = drafts.next(); + if drafts.next().is_some() { + return Err( + "Several behavior forms are pending; recovery cannot choose one to discard.".into(), + ); + } + first + .map(serde_json::to_value) + .transpose() + .map_err(|e| e.to_string()) + } + + /// Restore raw fields without planning/executing them. Rejected values and + /// incomplete endpoints must remain available for repair after a crash. + pub fn restore_recovery(&mut self, value: serde_json::Value) -> Result<(), String> { + let saved: RecoveryDraft = serde_json::from_value(value).map_err(|e| e.to_string())?; + if saved.version != 1 { + return Err(format!( + "Unsupported behavior recovery version {}", + saved.version + )); + } + let mut restored = Self::default(); + match saved.kind { + RecoveryKind::Creation => { + restored.creation = creation::State::from_recovery(saved.draft)? + } + RecoveryKind::State => restored.states = states::State::from_recovery(saved.draft)?, + RecoveryKind::Scenario => { + restored.scenario = scenario::State::from_recovery(saved.draft)? + } + RecoveryKind::Process => { + restored.topology = topology::State::from_recovery(saved.draft)? + } + } + restored.active_owner = restored.pending_owner(); + *self = restored; + Ok(()) + } + + pub fn has_pending(&self) -> bool { + self.creation.has_pending() || self.pending_owner().is_some() + } + + pub fn pending_owner(&self) -> Option { + self.topology + .pending_owner() + .or_else(|| self.scenario.pending_owner()) + .or_else(|| self.states.pending_owner()) + } + + /// Prepare the active form without discarding inputs. The caller must execute + /// this exact command and acknowledge only a successful document transaction. + pub fn prepare_pending(&self, p: &Project) -> Result, String> { + if self.creation.has_pending() { + self.creation.prepare_pending(p).map(Some) + } else if self.topology.pending_owner().is_some() { + self.topology.prepare_pending(p).map(Some) + } else if self.scenario.pending_owner().is_some() { + self.scenario.prepare_pending(p).map(Some) + } else if self.states.pending_owner().is_some() { + self.states.prepare_pending(p).map(Some) + } else { + Ok(None) + } + } + + /// Clear the active form after its prepared command commits successfully. + /// Rejected commands must leave the form available for repair and retry. + pub fn source_active(&self) -> bool { + self.creation.source_active() + || self.scenario.source_active() + || self.topology.source_active() + } + pub fn source_open(&self) -> bool { + self.creation.source_open() || self.scenario.source_open() || self.topology.source_open() + } + pub fn source_back(&mut self) -> bool { + self.creation.source_back() || self.scenario.source_back() || self.topology.source_back() + } + + pub fn acknowledge_commit(&mut self) { + self.creation.acknowledge_commit(); + self.topology.acknowledge_commit(); + self.scenario.acknowledge_commit(); + self.states.acknowledge_commit(); + } + + pub fn update(&mut self, p: &Project, event: Event) -> Result, String> { + if self.has_pending() + && matches!( + &event, + Event::Creation(creation::Event::Open(..)) + | Event::Topology(topology::Event::Start(..)) + | Event::Scenario(scenario::Event::Start(..)) + | Event::States(states::Event::Start(..) | states::Event::Edit(..)) + ) + { + return Err(PENDING_DRAFT.into()); + } + let cancels_pending = match &event { + Event::Topology(topology::Event::Cancel) => self.topology.pending_owner().is_some(), + Event::Scenario(scenario::Event::Cancel) => self.scenario.pending_owner().is_some(), + Event::States(states::Event::Cancel) => self.states.pending_owner().is_some(), + _ => false, + }; + if cancels_pending { + self.acknowledge_commit(); + return Ok(None); + } + match event { + Event::Creation(event) => self.creation.update(p, event), + Event::Topology(event) => self.topology.update(p, event), + Event::Scenario(event) => self.scenario.update(p, event), + Event::States(event) => self.states.update(p, event), + Event::Submit => self + .prepare_pending(p)? + .map(Some) + .ok_or_else(|| "Choose a behavior modeling tool.".into()), + } + } +} + +pub fn view(app: &App) -> Element<'_, Message> { + let p = app.project(); + if let Some(card) = app.behavior.creation.view(p) { + return card.map(|event| Message::Behavior(Event::Creation(event))); + } + if let Some(owner) = app.behavior.pending_owner() { + let kind = p.elements.get(&owner).map(oa_core::Element::kind); + let unavailable = p.layer_of(owner) != Some(app.layer) + || (app.behavior.scenario.pending_owner().is_some() && kind != Some(Kind::Scenario)) + || (app.behavior.topology.pending_owner().is_some() + && !matches!(kind, Some(Kind::Process | Kind::FunctionalChain))); + if unavailable { + let mut out=column![text("The pending behavior owner is unavailable").size(20),text("Your inputs are retained. Return from the source form or cancel this parent draft before choosing another behavior.").size(13)].spacing(12); + if let Some(name) = app + .behavior + .scenario + .pending_name() + .or_else(|| app.behavior.topology.pending_name()) + { + out = + out.push(text(format!("Pending consumer name: {}", short(name, 80))).size(14)); + } + if let Some(child) = app.behavior.scenario.source_view(p) { + out = out.push(child.map(|e| Message::Behavior(Event::Scenario(e)))); + } else if let Some(child) = app.behavior.topology.source_view(p) { + out = out.push(child.map(|e| Message::Behavior(Event::Topology(e)))); + } else { + let cancel = if app.behavior.scenario.pending_owner().is_some() { + Event::Scenario(scenario::Event::Cancel) + } else if app.behavior.topology.pending_owner().is_some() { + Event::Topology(topology::Event::Cancel) + } else { + Event::States(states::Event::Cancel) + }; + out = out.push(button("Cancel retained draft").on_press(Message::Behavior(cancel))); + } + return container(out).padding(16).width(Fill).height(Fill).into(); + } + } + let choices = p + .elements + .values() + .filter(|e| { + e.layer() == app.layer && (e.kind().is_sequence() || e.kind() == Kind::StateMachine) + }) + .map(|e| Choice { + id: e.id, + label: format!("{} · {}", e.kind(), e.name), + }) + .collect::>(); + let owner = selected_owner(app); + let selected = owner + .and_then(|id| choices.iter().find(|c| c.id == id)) + .or(choices.first()) + .cloned(); + let mut out = column![heading("Behavior & scenarios", "Follow exchanges and operating conditions. Select a message, state or transition to edit it.")].spacing(14); + let Some(selected) = selected else { + return out + .push(label( + "Start a named process, scenario or state machine, then model its behavior here.", + )) + .push(action( + "New", + Message::Behavior(Event::Creation(creation::Event::Open(app.layer))), + )) + .padding(24) + .into(); + }; + let e = &p.elements[&selected.id]; + let compact_canvas = matches!( + e.kind(), + Kind::Process | Kind::FunctionalChain | Kind::Scenario + ); + if e.kind() == Kind::StateMachine { + out = column![text("States & modes").size(20)].spacing(8); + } else if compact_canvas { + // The process/scenario canvas owns its compact remaining-height layout. + out = column![].spacing(6); + } + out = out.push( + row![ + pick_list(choices, Some(selected), Message::BehaviorOwner) + .width(Fill) + .text_size(13) + .padding(if compact_canvas { 6 } else { 10 }), + action( + "New", + Message::Behavior(Event::Creation(creation::Event::Open(app.layer))) + ) + ] + .spacing(8), + ); + if matches!(e.kind(), Kind::Process | Kind::FunctionalChain) { + return topology::view(app, e.id, out); + } + if e.kind() == Kind::Scenario { + return scenario::view(app, e.id, out); + } + state_view::view(app, e.id, out) +} + +pub fn selected_owner(app: &App) -> Option { + let valid = |id: &Id| { + app.project().elements.get(id).is_some_and(|e| { + e.layer() == app.layer && (e.kind().is_sequence() || e.kind() == Kind::StateMachine) + }) + }; + app.behavior + .pending_owner() + .or(app.behavior.active_owner.filter(valid)) + .or_else(|| selected_behavior(app.project(), app.selected).filter(valid)) + .or_else(|| app.project().elements.keys().find(|id| valid(id)).copied()) +} +/// Mirror the core's legacy-versus-explicit process interpretation. Existing +/// exchange-only lists still describe ordered paths until explicitly migrated. +fn uses_explicit_topology(p: &Project, owner: Id) -> bool { + let Some(e) = p.elements.get(&owner) else { + return false; + }; + if !matches!(e.kind(), Kind::Process | Kind::FunctionalChain) { + return false; + } + p.relations.values().any(|r| { + r.kind() == RelationKind::SequenceLink + && p.elements.get(&r.source()).and_then(|e| e.parent) == Some(owner) + && p.elements.get(&r.target()).and_then(|e| e.parent) == Some(owner) + }) || e.steps().into_iter().flatten().any(|id| { + matches!( + p.elements.get(id).and_then(|e| e.shared()), + Some(SharedBody::Occurrence { + body: OccurrenceBody::Action { .. } + | OccurrenceBody::Control { .. } + | OccurrenceBody::SequenceReference { .. }, + .. + }) + ) + }) +} +pub fn selected_behavior(p: &Project, selected: Id) -> Option { + if let Some(Relation { + body: RelationBody::ContextualLink { owner, .. }, + .. + }) = p.relations.get(&selected) + { + return p + .elements + .get(owner) + .filter(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain)) + .map(|e| e.id); + } + let mut current = Some( + p.relations + .get(&selected) + .filter(|r| { + matches!( + r.kind(), + RelationKind::StateTransition + | RelationKind::SequenceLink + | RelationKind::StateAction + ) + }) + .map(|r| r.source()) + .unwrap_or(selected), + ); + while let Some(id) = current { + let e = p.elements.get(&id)?; + if e.kind().is_sequence() || e.kind() == Kind::StateMachine { + return Some(id); + } + current = e.parent; + } + None +} +fn in_machine(p: &Project, id: Id, machine: Id) -> bool { + let mut current = Some(id); + while let Some(id) = current { + if id == machine { + return true; + } + current = p.elements.get(&id).and_then(|e| e.parent); + } + false +} + +struct BoxItem { + id: Id, + rect: Rectangle, + title: String, + detail: String, + marker: Option, +} +struct Line { + id: Id, + points: Vec, + label: String, + label_at: Point, + dashed: bool, +} +struct Scene { + boxes: Vec, + lines: Vec, + size: Size, + selected: Id, + scale: f32, + scroll_offset: iced::Vector, + camera_pan: iced::Vector, + interaction: Option<(scenario::Context, scenario::CanvasTool, bool)>, + pending_placement: Option<(scenario::Slot, Option)>, + lanes: BTreeMap, + slots: Vec, + rows: BTreeMap, + open_calls: Vec, + sequence_bounds: BTreeMap, +} +struct SequenceSlot { + slot: scenario::Slot, + y: f32, + label: String, +} +#[derive(Default)] +struct SequenceInput { + context: Option, + tool: scenario::CanvasTool, + pointer: Option, + bounds: Option, + source: Option<(Id, scenario::Slot)>, + press: Option, +} +#[derive(Clone, Copy)] +enum SequencePress { + Occurrence { + tool: scenario::Tool, + slot: scenario::Slot, + lifeline: Option, + }, + Message { + source: Id, + slot: scenario::Slot, + start: Point, + first: bool, + moved: bool, + }, + Reorder { + occurrence: Id, + parent: Id, + start: Point, + moved: bool, + }, +} +impl SequenceInput { + fn cancel(&mut self) { + self.source = None; + self.press = None; + } +} +impl Scene { + fn sequence(p: &Project, owner: Id, selected: Id) -> Self { + let mut scene = Self { + scale: 1., + scroll_offset: iced::Vector::ZERO, + camera_pan: iced::Vector::ZERO, + boxes: vec![], + lines: vec![], + size: Size::new(700., 200.), + selected, + interaction: None, + pending_placement: None, + lanes: BTreeMap::new(), + slots: vec![], + rows: BTreeMap::new(), + open_calls: vec![], + sequence_bounds: BTreeMap::new(), + }; + let mut lanes = p + .children(owner) + .filter(|e| e.kind() == Kind::Lifeline) + .map(|e| { + ( + e.id, + e.name.clone(), + match e.shared() { + Some(SharedBody::Lifeline { + represented: Some(id), + }) => p.named(*id).to_owned(), + _ => "Unassigned participant".into(), + }, + ) + }) + .collect::>(); + let scenario = p.elements[&owner].kind() == Kind::Scenario; + if !scenario { + fn endpoints(p: &Project, steps: &[Id], ids: &mut Vec) { + for id in steps { + if let Some(SharedBody::Occurrence { body, .. }) = + p.elements.get(id).and_then(|e| e.shared()) + { + match body { + OccurrenceBody::Exchange { + exchange: Some(id), .. + } => { + if let Some(r) = p.relations.get(id) { + for endpoint in [r.source(), r.target()] { + if !ids.contains(&endpoint) { + ids.push(endpoint); + } + } + } + } + OccurrenceBody::Fragment { branches, .. } => { + endpoints(p, branches, ids) + } + OccurrenceBody::Branch { occurrences, .. } => { + endpoints(p, occurrences, ids) + } + _ => {} + } + } + } + } + let mut ids = vec![]; + endpoints(p, p.elements[&owner].steps().unwrap_or(&vec![]), &mut ids); + lanes = ids + .into_iter() + .map(|id| { + ( + id, + p.named(id).to_owned(), + p.elements + .get(&id) + .map(|e| e.kind().label()) + .unwrap_or("Endpoint") + .into(), + ) + }) + .collect(); + } + let lane_x = lanes + .iter() + .enumerate() + .map(|(i, (id, ..))| (*id, 160. + i as f32 * 240.)) + .collect::>(); + scene.size.width = (lanes.len() as f32 * 240. + 80.).max(680.); + if scenario { + scene.lanes = lane_x + .iter() + .filter(|(id, _)| { + matches!( + p.elements.get(id).and_then(|e| e.shared()), + Some(SharedBody::Lifeline { + represented: Some(_) + }) + ) + }) + .map(|(id, x)| (*id, *x)) + .collect(); + } + let mut y = if scenario { 148. } else { 120. }; + scene.occurrences(p, owner, scenario, &lane_x, &mut y, 0); + if scenario { + let calls: Vec<_> = scene + .rows + .keys() + .filter_map(|&id| { + if let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + message_kind: MessageKind::Call, + receiver: Some(receiver), + .. + }, + .. + }) = p.elements[&id].shared() + { + Some((id, *lane_x.get(receiver)?)) + } else { + None + } + }) + .collect(); + for (call, x) in calls { + scene.activation_sequence(p, p.elements[&call].parent.unwrap(), call, x, None); + } + } + scene.size.height = (y + 45.).max(210.); + for (id, name, detail) in lanes { + let x = lane_x[&id]; + scene.lines.insert( + 0, + Line { + id, + points: vec![Point::new(x, 85.), Point::new(x, scene.size.height - 20.)], + label: String::new(), + label_at: Point::ORIGIN, + dashed: false, + }, + ); + scene.boxes.push(BoxItem { + id, + rect: Rectangle::new(Point::new(x - 102., 15.), Size::new(204., 70.)), + title: name, + detail, + marker: None, + }); + } + scene + } + fn occurrences( + &mut self, + p: &Project, + owner: Id, + scenario: bool, + lanes: &BTreeMap, + y: &mut f32, + depth: usize, + ) { + let steps = p + .elements + .get(&owner) + .and_then(|e| { + e.steps().map(Vec::as_slice).or_else(|| { + if let Some(SharedBody::Occurrence { + body: OccurrenceBody::Fragment { branches, .. }, + .. + }) = e.shared() + { + Some(branches.as_slice()) + } else { + None + } + }) + }) + .unwrap_or(&[]); + let ordered = p.elements.get(&owner).and_then(|e| e.steps()).is_some(); + let sequence_top = *y - 42.; + for (index, id) in steps.iter().enumerate() { + let Some(e) = p.elements.get(id) else { + continue; + }; + let Some(SharedBody::Occurrence { body, .. }) = e.shared() else { + continue; + }; + if ordered { + let slot = scenario::Slot { + parent: owner, + before: Some(*id), + }; + self.slots.push(SequenceSlot { + slot, + y: *y - 42., + label: scenario::slot_label(p, slot), + }); + self.rows.insert(*id, (owner, *y)); + } + match body { + OccurrenceBody::Fragment { operator, .. } => { + let top = *y; + *y += 55.; + self.occurrences(p, *id, scenario, lanes, y, depth + 1); + let rect = Rectangle::new( + Point::new(18. + depth as f32 * 10., top), + Size::new( + self.size.width - 36. - depth as f32 * 20., + (*y - top).max(95.), + ), + ); + self.boxes.insert( + 0, + BoxItem { + id: *id, + rect, + title: format!("{operator:?} · {}", e.name), + detail: String::new(), + marker: Some(Kind::Occurrence), + }, + ); + *y += if scenario { 64. } else { 20. }; + } + OccurrenceBody::Branch { guard, .. } => { + self.boxes.push(BoxItem { + id: *id, + rect: Rectangle::new( + Point::new(36. + depth as f32 * 10., *y), + Size::new(self.size.width - 85. - depth as f32 * 20., 43.), + ), + title: format!( + "[{}]", + if guard.is_empty() { + "guard not specified" + } else { + guard + } + ), + detail: String::new(), + marker: Some(Kind::Occurrence), + }); + *y += if scenario { 90. } else { 62. }; + self.occurrences(p, *id, scenario, lanes, y, depth + 1); + } + OccurrenceBody::Exchange { + exchange, + sender, + receiver, + message_kind, + .. + } => { + let r = exchange.and_then(|id| p.relations.get(&id)); + let source = if scenario { + *sender + } else { + r.map(|r| r.source()) + }; + let target = if scenario { + if matches!(message_kind, MessageKind::Timer { .. }) { + *sender + } else { + *receiver + } + } else { + r.map(|r| r.target()) + }; + if let (Some(a), Some(b)) = ( + source.and_then(|id| lanes.get(&id)), + target.and_then(|id| lanes.get(&id)), + ) { + let points = if a == b { + vec![ + Point::new(*a, *y), + Point::new(a + 75., *y), + Point::new(a + 75., *y + 24.), + Point::new(*a, *y + 24.), + ] + } else { + vec![Point::new(*a, *y), Point::new(*b, *y)] + }; + let kind = if !scenario { + "" + } else { + match message_kind { + MessageKind::Asynchronous => "signal · ", + MessageKind::Call => "call · ", + MessageKind::Reply => "reply · ", + MessageKind::Create => "create · ", + MessageKind::Delete => "delete · ", + MessageKind::Timer { .. } => "timer · ", + } + }; + let mut message_label = if !scenario { + r.map(|r| r.name.clone()).unwrap_or_else(|| e.name.clone()) + } else if let MessageKind::Timer { expression } = message_kind { + format!("{} · {expression}", e.name) + } else if let Some(exchange) = r.filter(|r| r.name != e.name) { + format!("{} · {}", e.name, exchange.name) + } else { + e.name.clone() + }; + if scenario + && let Some(operation) = crate::data_values::operation_label(p, e) + { + message_label.push_str(" · "); + message_label.push_str(&operation); + } + self.lines.push(Line { + id: *id, + points, + label: format!("{}. {kind}{}", index + 1, message_label), + label_at: Point::new(a.min(*b) + 12., *y - 26.), + dashed: matches!(message_kind, MessageKind::Reply), + }); + } else { + self.boxes.push(BoxItem { + id: *id, + rect: Rectangle::new( + Point::new(65., *y - 20.), + Size::new(self.size.width - 130., 65.), + ), + title: e.name.clone(), + detail: "Choose an exchange and its lifelines in Properties".into(), + marker: None, + }); + } + *y += 92.; + } + OccurrenceBody::Action { action, lifeline } + | OccurrenceBody::State { + state: action, + lifeline, + } => { + let x = lifeline + .and_then(|id| lanes.get(&id)) + .copied() + .unwrap_or(180.); + self.boxes.push(BoxItem { + id: *id, + rect: Rectangle::new(Point::new(x - 110., *y - 20.), Size::new(220., 70.)), + title: e.name.clone(), + detail: action + .map(|id| p.named(id).to_owned()) + .unwrap_or("Choose observed behavior".into()), + marker: None, + }); + *y += 95.; + } + OccurrenceBody::Control { kind } => { + self.boxes.push(BoxItem { + id: *id, + rect: Rectangle::new(Point::new(65., *y - 20.), Size::new(240., 70.)), + title: format!("{kind:?} · {}", e.name), + detail: "Contextual process control".into(), + marker: None, + }); + *y += 95.; + } + OccurrenceBody::SequenceReference { sequence } => { + self.boxes.push(BoxItem { + id: *id, + rect: Rectangle::new(Point::new(65., *y - 20.), Size::new(280., 70.)), + title: e.name.clone(), + detail: sequence + .map(|id| format!("Use {}", p.named(id))) + .unwrap_or("Choose a reusable process or chain".into()), + marker: None, + }); + *y += 95.; + } + OccurrenceBody::ScenarioUse { scenario, bindings } => { + self.boxes.push(BoxItem { + id: *id, + rect: Rectangle::new( + Point::new(65., *y - 20.), + Size::new(self.size.width - 130., 70.), + ), + title: format!("ref · {}", e.name), + detail: scenario + .map(|id| { + format!( + "{} · {} explicit lifeline bindings", + p.named(id), + bindings.len() + ) + }) + .unwrap_or("Choose a scenario".into()), + marker: Some(Kind::Occurrence), + }); + *y += 95.; + } + } + } + if ordered { + self.sequence_bounds.insert(owner, (sequence_top, *y - 42.)); + let slot = scenario::Slot { + parent: owner, + before: None, + }; + self.slots.push(SequenceSlot { + slot, + y: *y - 42., + label: scenario::slot_label(p, slot), + }); + } + } + fn activation_segment(&mut self, call: Id, x: f32, top: f32, bottom: f32) { + if bottom <= top { + return; + } + self.boxes.push(BoxItem { + id: call, + rect: Rectangle::new(Point::new(x - 4., top), Size::new(8., bottom - top)), + title: String::new(), + detail: String::new(), + marker: None, + }); + } + /// Follow a single correlation through the ordered tree. Branches are drawn + /// as independent segments, never as one chronological stretch through ALT + /// operands. The result says whether a path can leave this sequence open. + fn activation_sequence( + &mut self, + p: &Project, + owner: Id, + call: Id, + x: f32, + mut start: Option, + ) -> bool { + let entered_open = start.is_some(); + let mut split = false; + for id in p.elements[&owner].steps().into_iter().flatten() { + let Some(&(_, y)) = self.rows.get(id) else { + continue; + }; + if *id == call { + start = Some(y); + } + let Some(top) = start else { + continue; + }; + match p.elements[id].shared() { + Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + message_kind: MessageKind::Reply, + reply_to: Some(correlation), + .. + }, + .. + }) if *correlation == call => { + self.activation_segment(call, x, top, y); + self.boxes.push(BoxItem { + id: call, + rect: Rectangle::new(Point::new(x - 7., y - 1.), Size::new(14., 2.)), + title: String::new(), + detail: String::new(), + marker: None, + }); + start = None; + } + Some(SharedBody::Occurrence { + body: OccurrenceBody::Fragment { operator, branches }, + .. + }) => { + split = true; + self.activation_segment(call, x, top, y); + let bottom = self + .boxes + .iter() + .find(|b| b.id == *id && !b.title.is_empty()) + .map(|b| b.rect.y + b.rect.height) + .unwrap_or(y); + let outcomes: Vec<_> = branches + .iter() + .filter_map(|branch| { + let &(top, _) = self.sequence_bounds.get(branch)?; + Some(self.activation_sequence(p, *branch, call, x, Some(top))) + }) + .collect(); + let open = match operator { + FragmentOperator::Alternative => { + outcomes.is_empty() || outcomes.iter().any(|open| *open) + } + FragmentOperator::Parallel => outcomes.iter().all(|open| *open), + // Optional/repeated bodies do not guarantee execution. + FragmentOperator::Optional | FragmentOperator::Loop => true, + }; + start = open.then_some(bottom); + } + _ => {} + } + } + if let Some(top) = start { + let bottom = if entered_open || split { + self.sequence_bounds + .get(&owner) + .map_or(top + 26., |(_, bottom)| (*bottom).max(top + 26.)) + } else { + top + 26. + }; + self.activation_segment(call, x, top, bottom); + self.open_calls.push(Point::new(x + 8., bottom - 7.)); + } + start.is_some() + } + fn slot_at(&self, point: Point, parent: Option) -> Option<&SequenceSlot> { + self.slots + .iter() + .min_by(|a, b| (a.y - point.y).abs().total_cmp(&(b.y - point.y).abs())) + .filter(|slot| parent.is_none_or(|id| id == slot.slot.parent)) + } + fn lane_at(&self, point: Point) -> Option { + if let Some(header) = self + .boxes + .iter() + .find(|b| self.lanes.contains_key(&b.id) && b.rect.contains(point)) + { + return Some(header.id); + } + self.lanes + .iter() + .filter(|(_, x)| (**x - point.x).abs() < 12. / self.scale) + .min_by(|a, b| (a.1 - point.x).abs().total_cmp(&(b.1 - point.x).abs())) + .map(|(id, _)| *id) + } + fn reorder_handle(&self) -> Option { + let (_, y) = self.rows.get(&self.selected)?; + Some(Rectangle::new( + Point::new(6., *y - 12.), + Size::new(22., 24.), + )) + } + fn hit(&self, point: Point) -> Option { + self.boxes + .iter() + .rev() + .find(|b| { + !b.title.is_empty() + && b.rect.contains(point) + && (b.marker != Some(Kind::Occurrence) || point.y < b.rect.y + 38.) + }) + .map(|b| b.id) + .or_else(|| { + self.lines + .iter() + .find(|line| { + !line.label.is_empty() + && (Rectangle::new( + line.label_at, + Size::new( + (line.label.chars().count() as f32 * 6.8 + 8.).min(225.), + 25., + ), + ) + .contains(point) + || line.points.windows(2).any(|p| { + crate::diagram::distance_to_segment(point, p[0], p[1]) + < 8. / self.scale + })) + }) + .map(|line| line.id) + }) + .or_else(|| { + self.boxes + .iter() + .rev() + .find(|b| b.rect.contains(point) && b.title.is_empty()) + .map(|b| b.id) + }) + .or_else(|| self.lane_at(point)) + .or_else(|| { + self.lines + .iter() + .find(|line| { + line.label.is_empty() + && line.points.windows(2).any(|p| { + crate::diagram::distance_to_segment(point, p[0], p[1]) + < 8. / self.scale + }) + }) + .map(|line| line.id) + }) + } +} +impl canvas::Program for Scene { + type State = SequenceInput; + fn update( + &self, + state: &mut SequenceInput, + event: &canvas::Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + use scenario::{CanvasEvent, CanvasTool}; + let (context, tool, editable) = self + .interaction + .map_or((None, CanvasTool::Select, false), |(c, t, e)| { + (Some(c), t, e) + }); + if state.context != context || state.tool != tool { + state.cancel(); + state.pointer = None; + state.context = context; + state.tool = tool; + } + // Scrolling either enclosing Scrollable changes the canvas origin. A + // captured insertion slot belongs to the previous view, even if the + // pointer itself did not move and the semantic context is unchanged. + if state.bounds.is_some_and(|previous| previous != bounds) { + state.cancel(); + state.pointer = None; + } + state.bounds = Some(bounds); + if !editable { + state.cancel(); + } + // Scrollable marks a pointer outside its visible viewport unavailable, + // even when it lies inside the full (scrolled) canvas rectangle. + let visible = cursor.position().is_some(); + if !visible + && matches!( + event, + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + ) + { + state.cancel(); + return Some(Action::request_redraw()); + } + let cursor = match event { + canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => { + state.pointer = Some(*position); + if let Some(press) = &mut state.press { + match press { + SequencePress::Message { start, moved, .. } + | SequencePress::Reorder { start, moved, .. } => { + *moved |= position.distance(*start) >= 3. + } + SequencePress::Occurrence { .. } => {} + } + } + mouse::Cursor::Available(*position + self.scroll_offset) + } + canvas::Event::Mouse(mouse::Event::WheelScrolled { .. }) + | canvas::Event::Window(iced::window::Event::Resized(_)) + | canvas::Event::Mouse(mouse::Event::CursorLeft) + | canvas::Event::Window(iced::window::Event::Unfocused) => { + state.cancel(); + state.pointer = None; + return Some(Action::request_redraw()); + } + canvas::Event::Mouse( + mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_), + ) => state + .pointer + .map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)), + canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => { + state.pointer = cursor.position().map(|p| p - self.scroll_offset); + cursor + } + canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + .. + }) if state.source.is_some() || state.press.is_some() => { + state.cancel(); + return Some(Action::request_redraw().and_capture()); + } + _ => cursor, + }; + let world = cursor.position_in(bounds).map(|p| { + Point::new( + (p.x - self.camera_pan.x) / self.scale, + (p.y - self.camera_pan.y) / self.scale, + ) + }); + match event { + canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if !visible { + return None; + } + let point = world?; + if let Some(context) = context { + if !editable { + return None; + } + if let CanvasTool::Message(_) = tool { + let first = state.source.is_none(); + let (source, slot) = state.source.or_else(|| { + Some((self.lane_at(point)?, self.slot_at(point, None)?.slot)) + })?; + state.source = Some((source, slot)); + state.press = Some(SequencePress::Message { + source, + slot, + start: cursor.position()? - self.scroll_offset, + first, + moved: false, + }); + return Some(Action::request_redraw().and_capture()); + } + if let CanvasTool::Add(tool) = tool { + if !tool.canvas_occurrence() && tool != scenario::Tool::Lifeline { + return None; + } + state.press = Some(SequencePress::Occurrence { + tool, + slot: self.slot_at(point, None)?.slot, + lifeline: tool.uses_lifeline().then(|| self.lane_at(point)).flatten(), + }); + return Some(Action::request_redraw().and_capture()); + } + if let Some(occurrence) = self + .hit(point) + .filter(|id| self.rows.contains_key(id)) + .or_else(|| { + self.reorder_handle() + .filter(|r| r.contains(point)) + .map(|_| self.selected) + }) + { + state.press = Some(SequencePress::Reorder { + occurrence, + parent: self.rows[&occurrence].0, + start: cursor.position()? - self.scroll_offset, + moved: false, + }); + return Some(Action::request_redraw().and_capture()); + } + return self.hit(point).map(|id| { + Action::publish(Message::ScenarioCanvas(CanvasEvent::Select(context, id))) + .and_capture() + }); + } + self.hit(point) + .map(|id| Action::publish(Message::Select(id)).and_capture()) + } + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let press = state.press.take()?; + let context = context?; + match press { + SequencePress::Occurrence { + tool: captured, + slot, + lifeline, + } if tool == CanvasTool::Add(captured) && world.is_some() => { + return Some( + Action::publish(Message::ScenarioCanvas( + CanvasEvent::BeginOccurrence { + context, + tool: captured, + slot, + lifeline, + }, + )) + .and_capture(), + ); + } + SequencePress::Message { source, slot, .. } + if tool == CanvasTool::Message(scenario::MessageStyle::Timer) + && world.is_some() => + { + state.cancel(); + return Some( + Action::publish(Message::ScenarioCanvas(CanvasEvent::Begin { + context, + slot, + sender: source, + receiver: source, + style: scenario::MessageStyle::Timer, + })) + .and_capture(), + ); + } + SequencePress::Message { + source, + slot, + first, + moved, + .. + } if moved || !first => { + if let Some(receiver) = world.and_then(|p| self.lane_at(p)) { + let CanvasTool::Message(style) = tool else { + return None; + }; + state.cancel(); + return Some( + Action::publish(Message::ScenarioCanvas(CanvasEvent::Begin { + context, + slot, + sender: source, + receiver, + style, + })) + .and_capture(), + ); + } + } + SequencePress::Reorder { + occurrence, + moved: false, + .. + } => { + return Some( + Action::publish(Message::ScenarioCanvas(CanvasEvent::Select( + context, occurrence, + ))) + .and_capture(), + ); + } + SequencePress::Reorder { + occurrence, + parent, + moved: true, + .. + } => { + if let Some(slot) = world.and_then(|p| self.slot_at(p, Some(parent))) { + return Some( + Action::publish(Message::ScenarioCanvas(CanvasEvent::Reorder { + context, + occurrence, + slot: slot.slot, + })) + .and_capture(), + ); + } + } + _ => {} + } + Some(Action::request_redraw().and_capture()) + } + canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) if context.is_some() => { + Some(Action::request_redraw()) + } + _ => None, + } + } + fn draw( + &self, + state: &SequenceInput, + renderer: &Renderer, + _: &Theme, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Vec { + let mut f = Frame::new(renderer, bounds.size()); + f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE); + f.translate(self.camera_pan); + f.scale(self.scale); + for b in &self.boxes { + let outline = matches!( + b.marker, + Some(Kind::Region | Kind::StateMachine | Kind::Occurrence) + ); + let color = if b.id == self.selected { + style::TEAL + } else { + style::LINE + }; + if outline { + f.stroke( + &Path::rectangle(b.rect.position(), b.rect.size()), + Stroke::default().with_color(color), + ); + } else { + let path = Path::rounded_rectangle(b.rect.position(), b.rect.size(), 8.into()); + f.fill(&path, Color::from_rgb8(243, 248, 248)); + f.stroke( + &path, + Stroke::default() + .with_color(color) + .with_width(if b.id == self.selected { 2.5 } else { 1. }), + ); + } + } + for l in &self.lines { + let color = if l.id == self.selected { + style::TEAL + } else { + style::MUTED + }; + f.stroke( + &Path::new(|p| { + p.move_to(l.points[0]); + for point in &l.points[1..] { + p.line_to(*point); + } + }), + Stroke { + line_dash: canvas::LineDash { + segments: if l.dashed { &[6., 4.] } else { &[] }, + offset: 0, + }, + ..Stroke::default() + .with_color(color) + .with_width(if l.id == self.selected { 2.5 } else { 1.2 }) + }, + ); + if !l.label.is_empty() { + let end = l.points[l.points.len() - 1]; + let prev = l.points[l.points.len() - 2]; + let delta = end - prev; + let len = (delta.x * delta.x + delta.y * delta.y).sqrt().max(1.); + let d = delta / len; + f.stroke( + &Path::new(|p| { + p.move_to(end - d * 9. + iced::Vector::new(-d.y, d.x) * 4.); + p.line_to(end); + p.line_to(end - d * 9. - iced::Vector::new(-d.y, d.x) * 4.); + }), + Stroke::default().with_color(color), + ); + f.fill_rectangle( + l.label_at, + Size::new((l.label.chars().count() as f32 * 6.8 + 8.).min(225.), 22.), + Color::WHITE, + ); + f.fill_text(Text { + content: short(&l.label, 32), + position: l.label_at, + color, + size: 12.into(), + ..Default::default() + }); + } + } + for b in &self.boxes { + f.fill_text(Text { + content: short(&b.title, ((b.rect.width - 25.) / 7.) as usize), + position: Point::new(b.rect.x + 12., b.rect.y + 12.), + size: 13.into(), + color: if b.id == self.selected { + style::TEAL + } else { + style::INK + }, + ..Default::default() + }); + if !b.detail.is_empty() { + f.fill_text(Text { + content: short(&b.detail, ((b.rect.width - 25.) / 6.) as usize), + position: Point::new(b.rect.x + 12., b.rect.y + 39.), + size: 11.into(), + color: style::MUTED, + ..Default::default() + }); + } + } + for point in &self.open_calls { + f.fill_text(Text { + content: "…".into(), + position: *point, + size: 11.into(), + color: style::MUTED, + ..Default::default() + }); + } + if let Some((context, tool, editable)) = self.interaction { + let active = editable && state.context == Some(context) && state.tool == tool; + let point = cursor.position_in(bounds).map(|p| { + Point::new( + (p.x - self.camera_pan.x) / self.scale, + (p.y - self.camera_pan.y) / self.scale, + ) + }); + let source = active.then_some(state.source).flatten(); + let reorder = active.then_some(state.press).flatten().and_then(|press| { + if let SequencePress::Reorder { parent, .. } = press { + Some(parent) + } else { + None + } + }); + if editable + && tool == scenario::CanvasTool::Select + && let Some(handle) = self.reorder_handle() + { + f.fill_rectangle(handle.position(), handle.size(), style::TEAL); + f.fill_text(Text { + content: "↕".into(), + position: handle.position() + iced::Vector::new(5., 2.), + size: 16.into(), + color: Color::WHITE, + ..Default::default() + }); + } + let placement = self.pending_placement.or_else(|| { + active.then_some(state.press).flatten().and_then(|press| { + if let SequencePress::Occurrence { slot, lifeline, .. } = press { + Some((slot, lifeline)) + } else { + None + } + }) + }); + let slot = if let Some((slot, _)) = placement { + self.slots.iter().find(|s| s.slot == slot) + } else if let Some((_, slot)) = source { + self.slots.iter().find(|s| s.slot == slot) + } else if editable + && (matches!( + tool, + scenario::CanvasTool::Message(_) | scenario::CanvasTool::Add(_) + ) || reorder.is_some()) + { + point.and_then(|p| self.slot_at(p, reorder)) + } else { + None + }; + if reorder.is_some() + && slot.is_none() + && let Some(point) = point + { + f.fill_text(Text { + content: "Other operand: move stays in the current sequence".into(), + position: Point::new(point.x + 8., point.y + 12.), + size: (11. / self.scale).into(), + color: Color::from_rgb(0.75, 0.2, 0.15), + ..Default::default() + }); + } + if let Some(slot) = slot { + f.stroke( + &Path::line( + Point::new(32., slot.y), + Point::new(self.size.width - 24., slot.y), + ), + Stroke::default().with_color(style::TEAL).with_width(1.5), + ); + f.fill_rectangle( + Point::new(34., slot.y - 20.), + Size::new((self.size.width - 70.).min(650.), 19.), + Color::WHITE, + ); + f.fill_text(Text { + content: short( + &format!( + "{}: {}", + if self.pending_placement.is_some() { + "Pending" + } else { + "Insert" + }, + slot.label + ), + 88, + ), + position: Point::new(38., slot.y - 19.), + size: 11.into(), + color: style::TEAL, + ..Default::default() + }); + if let Some(x) = placement + .and_then(|(_, lifeline)| lifeline) + .and_then(|id| self.lanes.get(&id)) + { + f.fill(&Path::circle(Point::new(*x, slot.y), 5.), style::TEAL); + } + if let Some((source, _)) = source + && let Some(x) = self.lanes.get(&source) + && let Some(point) = point + { + let to = self + .lane_at(point) + .and_then(|id| self.lanes.get(&id)) + .copied() + .unwrap_or(point.x); + f.stroke( + &Path::line(Point::new(*x, slot.y), Point::new(to, slot.y)), + Stroke::default().with_color(style::TEAL).with_width(3.), + ); + f.fill(&Path::circle(Point::new(to, slot.y), 5.), style::TEAL); + } + } + } + vec![f.into_geometry()] + } + fn mouse_interaction( + &self, + _: &SequenceInput, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> mouse::Interaction { + if cursor.is_over(bounds) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } +} + +#[cfg(test)] +mod direct_canvas_tests { + use super::*; + use scenario::{CanvasEvent, CanvasTool, MessageStyle}; + fn scene() -> (scenario::direct_tests::Fixture, Scene) { + let f = scenario::direct_tests::fixture(); + let mut scene = Scene::sequence(&f.project, f.owner, f.call); + scene.interaction = Some(( + scenario::direct_tests::context(f.owner), + CanvasTool::Message(MessageStyle::CallReply), + true, + )); + (f, scene) + } + fn send( + scene: &Scene, + state: &mut SequenceInput, + event: mouse::Event, + final_cursor: Point, + bounds: Rectangle, + ) -> Option { + canvas::Program::update( + scene, + state, + &canvas::Event::Mouse(event), + bounds, + mouse::Cursor::Available(final_cursor), + ) + .and_then(|action| action.into_inner().0) + .and_then(|message| { + if let Message::ScenarioCanvas(event) = message { + Some(event) + } else { + None + } + }) + } + #[test] + fn reused_exchange_messages_display_their_distinct_occurrence_names() { + let mut f = scenario::direct_tests::fixture(); + f.project.elements.get_mut(&f.call).unwrap().name = "Route request".into(); + f.project.elements.get_mut(&f.reply).unwrap().name = "Route response".into(); + let scene = Scene::sequence(&f.project, f.owner, f.call); + let call = scene.lines.iter().find(|line| line.id == f.call).unwrap(); + let reply = scene.lines.iter().find(|line| line.id == f.reply).unwrap(); + assert!(call.label.starts_with("1. call · Route request")); + assert!(reply.label.starts_with("2. reply · Route response")); + assert!(call.label.ends_with(&f.project.relations[&f.exchange].name)); + assert!( + reply + .label + .ends_with(&f.project.relations[&f.exchange].name) + ); + } + #[test] + fn direct_drag_and_click_click_use_ordered_sources_and_publish_once() { + for drag in [false, true] { + let (f, scene) = scene(); + let mut state = SequenceInput::default(); + let bounds = Rectangle::new(Point::new(30., 50.), scene.size); + let a = Point::new( + bounds.x + scene.lanes[&f.sender], + bounds.y + scene.slots[0].y, + ); + let b = Point::new(bounds.x + scene.lanes[&f.receiver], a.y); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: a }, + b, + bounds, + ); + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + b, + bounds + ) + .is_none() + ); + if !drag { + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + bounds + ) + .is_none() + ); + assert_eq!(state.source.unwrap().0, f.sender); + } + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + if !drag { + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + b, + bounds, + ); + } + let result = send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + bounds, + ); + assert!( + matches!(result, Some(CanvasEvent::Begin { sender, receiver, slot, .. }) if sender == f.sender && receiver == f.receiver && slot.before == Some(f.call)) + ); + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + bounds + ) + .is_none() + ); + assert!(state.source.is_none() && state.press.is_none()); + } + } + #[test] + fn direct_scrolled_canvas_maps_raw_pointer_events_to_the_visible_slot() { + let (f, mut scene) = scene(); + scene.scroll_offset = iced::Vector::new(35., 140.); + let bounds = Rectangle::new(Point::new(20., 230.), scene.size); + let slot = scene.slots.last().unwrap().slot; + let y = scene.slots.last().unwrap().y; + let a = Point::new(bounds.x + scene.lanes[&f.sender], bounds.y + y) - scene.scroll_offset; + let b = Point::new(bounds.x + scene.lanes[&f.receiver], bounds.y + y) - scene.scroll_offset; + let mut state = SequenceInput::default(); + // Iced Scrollable passes raw event coordinates and a translated cursor. + for event in [ + mouse::Event::CursorMoved { position: a }, + mouse::Event::ButtonPressed(mouse::Button::Left), + mouse::Event::CursorMoved { position: b }, + ] { + send(&scene, &mut state, event, b + scene.scroll_offset, bounds); + } + assert!( + matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), b + scene.scroll_offset, bounds), Some(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if sender == f.sender && receiver == f.receiver && actual == slot) + ); + } + #[test] + fn direct_scroll_resize_and_bounds_changes_cancel_captured_slots() { + for change in 0..5 { + let (f, scene) = scene(); + let mut state = SequenceInput::default(); + let bounds = Rectangle::new(Point::new(20., 70.), scene.size); + let a = Point::new( + bounds.x + scene.lanes[&f.sender], + bounds.y + scene.slots[0].y, + ); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: a }, + a, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + a, + bounds, + ); + assert!(state.source.is_some()); + let changed = match change { + 0 => Rectangle::new( + bounds.position() - iced::Vector::new(15., 32.), + bounds.size(), + ), + 1 => Rectangle::new(bounds.position(), bounds.size() + Size::new(20., 10.)), + _ => bounds, + }; + let event = match change { + 2 => canvas::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { x: 0., y: -1. }, + }), + 3 => canvas::Event::Window(iced::window::Event::Resized(Size::new(1100., 700.))), + _ => canvas::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )), + }; + let cursor = if change == 4 { + mouse::Cursor::Unavailable + } else { + mouse::Cursor::Available(a) + }; + let event = if change == 4 { + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + } else { + event + }; + canvas::Program::update(&scene, &mut state, &event, changed, cursor); + assert!( + state.source.is_none() && state.press.is_none(), + "change {change} must cancel the old slot" + ); + let b = Point::new( + changed.x + scene.lanes[&f.receiver], + changed.y + scene.slots.last().unwrap().y, + ); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: b }, + b, + changed, + ); + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + b, + changed, + ); + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + changed + ) + .is_none(), + "a click after change {change} must start a fresh gesture" + ); + assert_eq!(state.source.unwrap().0, f.receiver); + assert_ne!(state.source.unwrap().1.before, Some(f.call)); + } + } + #[test] + fn direct_bounds_change_during_held_drag_never_commits_old_slot() { + let (f, scene) = scene(); + let mut state = SequenceInput::default(); + let bounds = Rectangle::new(Point::ORIGIN, scene.size); + let a = Point::new(scene.lanes[&f.sender], scene.slots[0].y); + let b = Point::new(scene.lanes[&f.receiver], a.y); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: a }, + a, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + let shifted = Rectangle::new(Point::new(0., -40.), scene.size); + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + shifted + ) + .is_none() + ); + assert!(state.source.is_none() && state.press.is_none()); + } + #[test] + fn direct_context_escape_unfocus_and_pending_form_cancel_gestures() { + for cancellation in 0..5 { + let (f, mut scene) = scene(); + let mut state = SequenceInput::default(); + let bounds = Rectangle::new(Point::ORIGIN, scene.size); + let a = Point::new(scene.lanes[&f.sender], scene.slots[0].y); + let b = Point::new(scene.lanes[&f.receiver], a.y); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: a }, + a, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + match cancellation { + 0 => { + scene.interaction.as_mut().unwrap().0.revision += 1; + } + 1 => { + scene.interaction.as_mut().unwrap().1 = CanvasTool::Select; + } + 2 => { + scene.interaction.as_mut().unwrap().2 = false; + } + 3 => { + canvas::Program::update( + &scene, + &mut state, + &canvas::Event::Window(iced::window::Event::Unfocused), + bounds, + mouse::Cursor::Unavailable, + ); + } + _ => { + canvas::Program::update( + &scene, + &mut state, + &canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + modified_key: iced::keyboard::Key::Named( + iced::keyboard::key::Named::Escape, + ), + physical_key: iced::keyboard::key::Physical::Code( + iced::keyboard::key::Code::Escape, + ), + location: iced::keyboard::Location::Standard, + modifiers: iced::keyboard::Modifiers::empty(), + text: None, + repeat: false, + }), + bounds, + mouse::Cursor::Available(b), + ); + } + } + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + bounds + ) + .is_none() + ); + assert!(state.press.is_none() && state.source.is_none()); + } + } + #[test] + fn direct_clicked_occurrence_capture_cancels_on_view_and_context_changes() { + for tool in [ + scenario::Tool::Action, + scenario::Tool::Observation, + scenario::Tool::Reuse, + scenario::Tool::Fragment, + ] { + for cancellation in 0..9 { + let (f, mut scene) = scene(); + scene.interaction.as_mut().unwrap().1 = CanvasTool::Add(tool); + let mut state = SequenceInput::default(); + let mut bounds = Rectangle::new(Point::ORIGIN, scene.size); + let point = Point::new(scene.lanes[&f.sender], scene.slots[0].y); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: point }, + point, + bounds, + ); + assert!( + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + bounds + ) + .is_none() + ); + assert!(matches!( + state.press, + Some(SequencePress::Occurrence { .. }) + )); + let cancellation_event = match cancellation { + 0 => { + scene.interaction.as_mut().unwrap().0.epoch += 1; + None + } + 1 => { + scene.interaction.as_mut().unwrap().1 = CanvasTool::Select; + None + } + 2 => { + scene.interaction.as_mut().unwrap().2 = false; + None + } + 3 => Some(canvas::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { x: 0., y: -1. }, + })), + 4 => Some(canvas::Event::Window(iced::window::Event::Resized( + Size::new(1100., 700.), + ))), + 5 => { + bounds.y -= 30.; + None + } + 6 => Some(canvas::Event::Window(iced::window::Event::Unfocused)), + 7 => Some(canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + modified_key: iced::keyboard::Key::Named( + iced::keyboard::key::Named::Escape, + ), + physical_key: iced::keyboard::key::Physical::Code( + iced::keyboard::key::Code::Escape, + ), + location: iced::keyboard::Location::Standard, + modifiers: iced::keyboard::Modifiers::empty(), + text: None, + repeat: false, + })), + _ => None, + }; + if let Some(event) = cancellation_event { + let action = canvas::Program::update( + &scene, + &mut state, + &event, + bounds, + mouse::Cursor::Available(point), + ); + assert!(action.and_then(|action| action.into_inner().0).is_none()); + } + let cursor = if cancellation == 8 { + mouse::Cursor::Unavailable + } else { + mouse::Cursor::Available(point) + }; + let released = canvas::Program::update( + &scene, + &mut state, + &canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + bounds, + cursor, + ); + assert!( + released.and_then(|action| action.into_inner().0).is_none(), + "{tool:?} cancellation {cancellation}" + ); + assert!(state.press.is_none()); + } + } + } + #[test] + fn direct_selected_handle_reorders_and_hits_keep_semantic_identity() { + let (f, mut scene) = scene(); + scene.interaction.as_mut().unwrap().1 = CanvasTool::Select; + let bounds = Rectangle::new(Point::ORIGIN, scene.size); + let header = scene + .boxes + .iter() + .find(|b| b.id == f.sender) + .unwrap() + .rect + .center(); + assert_eq!(scene.hit(header), Some(f.sender)); + assert_eq!( + scene.hit(Point::new(scene.lanes[&f.sender], scene.size.height - 28.)), + Some(f.sender) + ); + let line = scene.lines.iter().find(|line| line.id == f.reply).unwrap(); + assert_eq!( + scene.hit(line.label_at + iced::Vector::new(4., 4.)), + Some(f.reply) + ); + let mut state = SequenceInput::default(); + let start = scene.reorder_handle().unwrap().center(); + let end = Point::new(start.x, scene.slots.last().unwrap().y); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: start }, + start, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::ButtonPressed(mouse::Button::Left), + start, + bounds, + ); + send( + &scene, + &mut state, + mouse::Event::CursorMoved { position: end }, + end, + bounds, + ); + assert!( + matches!(send(&scene, &mut state, mouse::Event::ButtonReleased(mouse::Button::Left), end, bounds), Some(CanvasEvent::Reorder { occurrence, slot, .. }) if occurrence == f.call && slot.parent == f.owner && slot.before.is_none()) + ); + } + #[test] + fn direct_alt_execution_stops_each_operand_at_its_reply_and_marks_open_operand() { + use super::topology::occurrence; + let f = scenario::direct_tests::fixture(); + let mut p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap(); + let fragment = occurrence( + &p, + f.owner, + "Alternatives", + OccurrenceBody::Fragment { + operator: FragmentOperator::Alternative, + branches: vec![], + }, + ); + let fragment_id = fragment.id; + let mut commands = vec![Command::CreateElement(fragment)]; + let mut branches = vec![]; + let mut replies = vec![]; + let mut actions = vec![]; + for index in 0..3 { + let mut branch = occurrence( + &p, + f.owner, + &format!("Operand {index}"), + OccurrenceBody::Branch { + guard: format!("path {index}"), + occurrences: vec![], + }, + ); + branch.parent = Some(fragment_id); + let branch_id = branch.id; + branches.push(branch_id); + commands.push(Command::CreateElement(branch)); + let mut steps = vec![]; + if index < 2 { + let mut reply = occurrence( + &p, + f.owner, + &format!("Reply {index}"), + OccurrenceBody::Exchange { + operation: None, + exchange: Some(f.exchange), + sender: Some(f.receiver), + receiver: Some(f.sender), + message_kind: MessageKind::Reply, + reply_to: Some(f.call), + values: vec![], + }, + ); + reply.parent = Some(branch_id); + replies.push(reply.id); + steps.push(reply.id); + commands.push(Command::CreateElement(reply)); + } + let mut action = occurrence( + &p, + f.owner, + &format!("Action {index}"), + OccurrenceBody::Action { + action: Some(p.relations[&f.exchange].target()), + lifeline: Some(f.receiver), + }, + ); + action.parent = Some(branch_id); + actions.push(action.id); + steps.push(action.id); + commands.push(Command::CreateElement(action)); + commands.push(Command::SetSteps(branch_id, steps)); + } + if let Command::CreateElement(element) = &mut commands[0] + && let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Fragment { + branches: stored, .. + }, + .. + }) = element.shared_mut() + { + *stored = branches.clone(); + } + p = execute_batch(&p, &commands).unwrap(); + let scene = Scene::sequence(&p, f.owner, f.call); + let bars: Vec<_> = scene + .boxes + .iter() + .filter(|b| b.id == f.call && b.rect.width == 8.) + .map(|b| b.rect) + .collect(); + for reply in &replies { + let y = scene.rows[reply].1; + assert!( + bars.iter().any(|r| r.y <= y && r.y + r.height == y), + "every reply terminates its own operand segment" + ); + } + for action in &actions[..2] { + let y = scene.rows[action].1; + assert!( + !bars.iter().any(|r| r.y <= y && r.y + r.height > y), + "a returned operand must not remain active through its following action" + ); + } + let last_action = scene.rows[&actions[2]].1; + assert!( + scene.open_calls.iter().any(|p| p.y > last_action), + "the reply-free operand remains explicitly open" + ); + let header = scene + .boxes + .iter() + .find(|b| b.id == branches[1] && !b.title.is_empty()) + .unwrap() + .rect; + assert!( + !bars + .iter() + .any(|r| r.y < header.y + header.height && r.y + r.height > header.y), + "segments do not join mutually exclusive operand headers" + ); + } + #[test] + fn direct_execution_span_ends_at_its_correlated_reply_and_marks_an_open_call() { + let (f, scene) = scene(); + let bar = scene + .boxes + .iter() + .find(|b| b.id == f.call && b.rect.width == 8.) + .unwrap(); + assert_eq!(bar.rect.y + bar.rect.height, scene.rows[&f.reply].1); + assert!(scene.open_calls.is_empty()); + let p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap(); + let scene = Scene::sequence(&p, f.owner, f.call); + assert_eq!(scene.open_calls.len(), 1); + assert_eq!( + scene + .boxes + .iter() + .find(|b| b.id == f.call && b.rect.width == 8.) + .unwrap() + .rect + .height, + 26. + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recovery_retains_each_incomplete_behavior_form_without_executing_it() { + let p = Project::demo(); + let before = p.clone(); + let owner = |kind| p.elements.values().find(|e| e.kind() == kind).unwrap().id; + let machine = owner(Kind::StateMachine); + let scenario = owner(Kind::Scenario); + let process = owner(Kind::Process); + let cases = [ + ( + machine, + vec![ + Event::States(states::Event::Start( + machine, + states::Tool::Transition, + machine, + )), + Event::States(states::Event::Name("Unfinished timeout".into())), + Event::States(states::Event::Timed(true)), + Event::States(states::Event::Expression("-".into())), + Event::States(states::Event::Guard("temperature >".into())), + ], + ), + ( + scenario, + vec![ + Event::Scenario(scenario::Event::Start(scenario, scenario::Tool::Message)), + Event::Scenario(scenario::Event::Name("Unfinished request".into())), + Event::Scenario(scenario::Event::Expression("-".into())), + ], + ), + ( + process, + vec![ + Event::Topology(topology::Event::Start( + process, + topology::Tool::Connect, + None, + )), + Event::Topology(topology::Event::Name("Unfinished path".into())), + Event::Topology(topology::Event::Guard("available &&".into())), + ], + ), + ]; + for (owner, events) in cases { + let mut original = State::default(); + for event in events { + original.update(&p, event).unwrap(); + } + assert!(original.prepare_pending(&p).is_err()); + let snapshot = original.recovery_snapshot().unwrap().unwrap(); + let bytes = serde_json::to_vec(&snapshot).unwrap(); + let mut recovered = State::default(); + recovered + .restore_recovery(serde_json::from_slice(&bytes).unwrap()) + .unwrap(); + assert_eq!(recovered.pending_owner(), Some(owner)); + assert_eq!(recovered.active_owner, Some(owner)); + assert!( + recovered.prepare_pending(&p).is_err(), + "An incomplete recovered form is not applied or silently repaired" + ); + assert_eq!(recovered.recovery_snapshot().unwrap(), Some(snapshot)); + assert_eq!(p, before); + recovered.acknowledge_commit(); + assert!(recovered.recovery_snapshot().unwrap().is_none()); + } + } + + #[test] + fn recovery_preserves_distinct_qualified_value_paths_and_rejects_invalid_payload_atomically() { + let p = Project::demo(); + let scenario = p + .elements + .values() + .find(|e| e.kind() == Kind::Scenario) + .unwrap() + .id; + let field = Id::new_v4(); + let paths = [Id::new_v4(), Id::new_v4()]; + let mut state = State::default(); + state + .update( + &p, + Event::Scenario(scenario::Event::Start(scenario, scenario::Tool::Message)), + ) + .unwrap(); + for (path, value) in paths.into_iter().zip(["-", "3.4e"]) { + state + .update( + &p, + Event::Scenario(scenario::Event::Value( + crate::data_values::FieldPathKey { + path: vec![path], + field, + }, + value.into(), + )), + ) + .unwrap(); + } + let original = state.recovery_snapshot().unwrap().unwrap(); + let mut recovered = State::default(); + recovered.restore_recovery(original.clone()).unwrap(); + assert_eq!( + recovered.recovery_snapshot().unwrap(), + Some(original.clone()) + ); + let values = original["draft"]["values"].as_array().unwrap(); + assert_eq!(values.len(), 2); + assert_ne!(values[0][0], values[1][0]); + assert_eq!(values[0][1], values[1][1]); + for bad in [ + { + let mut value = original.clone(); + value["version"] = 99.into(); + value + }, + { + let mut value = original.clone(); + value["draft"]["unknown"] = true.into(); + value + }, + { + let mut value = original.clone(); + value["draft"]["values"] + .as_array_mut() + .unwrap() + .push(values[0].clone()); + value + }, + ] { + assert!(recovered.restore_recovery(bad).is_err()); + assert_eq!( + recovered.recovery_snapshot().unwrap(), + Some(original.clone()), + "Malformed recovery cannot discard the currently retained form" + ); + } + } + use crate::document::{Document, DocumentError}; + use oa_core::source_project::{self, ProjectLimits}; + + #[test] + fn one_behavior_form_survives_start_edit_and_unrelated_cancel_until_commit() { + let p = Project::demo(); + let owner = |kind| p.elements.values().find(|e| e.kind() == kind).unwrap().id; + let process = owner(Kind::Process); + let scenario = owner(Kind::Scenario); + let machine = owner(Kind::StateMachine); + let transition = p + .relations + .values() + .find(|r| r.kind() == RelationKind::StateTransition) + .unwrap() + .id; + let starts = [ + Event::Topology(topology::Event::Start( + process, + topology::Tool::Control, + None, + )), + Event::Scenario(scenario::Event::Start(scenario, scenario::Tool::Fragment)), + Event::States(states::Event::Start(machine, states::Tool::Region, machine)), + ]; + let names = [ + Event::Topology(topology::Event::Name("Keep this draft".into())), + Event::Scenario(scenario::Event::Name("Keep this draft".into())), + Event::States(states::Event::Name("Keep this draft".into())), + ]; + let cancels = [ + Event::Topology(topology::Event::Cancel), + Event::Scenario(scenario::Event::Cancel), + Event::States(states::Event::Cancel), + ]; + for (index, expected_owner) in [process, scenario, machine].into_iter().enumerate() { + let mut state = State::default(); + assert!(!state.has_pending()); + assert!(state.prepare_pending(&p).unwrap().is_none()); + state.update(&p, starts[index].clone()).unwrap(); + state.update(&p, names[index].clone()).unwrap(); + if index == 1 { + state + .update( + &p, + Event::Scenario(scenario::Event::Expression("approved".into())), + ) + .unwrap(); + } + assert_eq!(state.pending_owner(), Some(expected_owner)); + for replacement in starts + .iter() + .cloned() + .chain([Event::States(states::Event::Edit(transition))]) + { + assert_eq!(state.update(&p, replacement).unwrap_err(), PENDING_DRAFT); + assert_eq!(state.pending_owner(), Some(expected_owner)); + } + state.update(&p, cancels[(index + 1) % 3].clone()).unwrap(); + assert!(state.has_pending(), "Another editor's cancel is stale"); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + assert!(state.has_pending(), "Preparation must retain the form"); + let expected = execute_batch(&p, std::slice::from_ref(&command)).unwrap(); + assert!(expected.elements.values().any(|e| { + !p.elements.contains_key(&e.id) + && e.name == "Keep this draft" + && e.parent == Some(expected_owner) + })); + let mut document = Document::from_project(p.clone(), ProjectLimits::default()).unwrap(); + document.execute(command).unwrap(); + assert_eq!(document.project(), &expected); + state.acknowledge_commit(); + assert!(!state.has_pending()); + assert!(state.prepare_pending(document.project()).unwrap().is_none()); + assert_eq!(document.revision(), 1); + assert!(document.undo()); + assert_eq!(document.project(), &p); + assert!(!document.can_undo()); + + state.update(&p, starts[index].clone()).unwrap(); + state.update(&p, cancels[index].clone()).unwrap(); + assert!(!state.has_pending(), "Explicit cancel discards this form"); + state.update(&p, starts[(index + 1) % 3].clone()).unwrap(); + } + } + + #[test] + fn failed_document_commit_retains_typed_transition_and_time_event_for_repair() { + let mut p = Project::empty(); + let owner = p + .add_element(Kind::System, Layer::Sa, "System", None) + .unwrap(); + let machine = p + .add_element(Kind::StateMachine, Layer::Sa, "Modes", Some(owner)) + .unwrap(); + let region = p + .add_element(Kind::Region, Layer::Sa, "Region", Some(machine)) + .unwrap(); + let source = p + .add_element(Kind::State, Layer::Sa, "Ready", Some(region)) + .unwrap(); + let target = p + .add_element(Kind::State, Layer::Sa, "Waiting", Some(region)) + .unwrap(); + let work = p + .add_element(Kind::Function, Layer::Sa, "Monitor", None) + .unwrap(); + let choice = |id| Choice { + id, + label: p.named(id).into(), + }; + let mut state = State::default(); + for event in [ + states::Event::Start(machine, states::Tool::Transition, source), + states::Event::Name("Transition".repeat(1024)), + states::Event::Trigger(choice(work)), + states::Event::Effect(choice(work)), + states::Event::TriggerNotes("On a monitor result".into()), + states::Event::Guard("connection unavailable".into()), + states::Event::EffectNotes("Record the timeout".into()), + states::Event::Timed(true), + states::Event::TimeKind("At".into()), + states::Event::Expression("12:30 UTC".into()), + ] { + state.update(&p, Event::States(event)).unwrap(); + } + assert!( + state.prepare_pending(&p).is_err(), + "A target is still missing" + ); + assert_eq!(state.pending_owner(), Some(machine)); + state + .update(&p, Event::States(states::Event::Target(choice(target)))) + .unwrap(); + let command = state.prepare_pending(&p).unwrap().unwrap(); + let bundle = source_project::format_bundle(&p); + let mut limits = ProjectLimits::default(); + limits.model.max_source_bytes = + bundle.files.values().map(String::len).sum::() + 2000; + let mut document = Document::from_bundle(bundle.clone(), limits).unwrap(); + assert!(matches!( + document.execute(command), + Err(DocumentError::Source(_)) + )); + assert_eq!(document.project(), &p); + assert_eq!(document.bundle(), &bundle); + assert_eq!(document.revision(), 0); + assert!(!document.can_undo()); + assert!(state.has_pending()); + + state + .update( + &p, + Event::States(states::Event::Name("Wait for recovery".into())), + ) + .unwrap(); + let command = state.prepare_pending(&p).unwrap().unwrap(); + let Command::Batch(commands) = &command else { + panic!("Expected one atomic transition batch") + }; + assert_eq!(commands.len(), 2); + let Command::CreateElement(event) = &commands[0] else { + panic!("Expected a typed time event") + }; + assert!( + matches!(event.shared(), Some(SharedBody::Event { body: EventBody::Time { kind: TimeEventKind::At, expression } }) if expression == "12:30 UTC") + ); + let Command::CreateRelation(relation) = &commands[1] else { + panic!("Expected the transition") + }; + let RelationBody::StateTransition { + triggers, + effects, + trigger, + guard, + effect, + .. + } = &relation.body + else { + panic!() + }; + assert_eq!(triggers, &[work, event.id]); + assert_eq!(effects, &[work]); + assert_eq!(trigger, "On a monitor result"); + assert_eq!(guard, "connection unavailable"); + assert_eq!(effect, "Record the timeout"); + let expected = execute_batch(&p, std::slice::from_ref(&command)).unwrap(); + document.execute(command).unwrap(); + assert_eq!( + document.project(), + &expected, + "Commit the exact prepared identities" + ); + state.acknowledge_commit(); + assert!(!state.has_pending()); + assert_eq!(document.revision(), 1); + assert!(document.undo()); + assert_eq!(document.bundle(), &bundle); + assert_eq!(document.project(), &p); + assert!(!document.can_undo()); + } + + #[test] + fn legacy_process_and_chain_keep_their_ordered_exchange_scene() { + let p = Project::demo(); + let owners: Vec<_> = p + .elements + .values() + .filter(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain)) + .collect(); + assert!(owners.iter().any(|e| e.kind() == Kind::Process)); + assert!(owners.iter().any(|e| e.kind() == Kind::FunctionalChain)); + for owner in owners { + assert!(!uses_explicit_topology(&p, owner.id)); + let expected: Vec<_> = owner + .steps() + .unwrap() + .iter() + .enumerate() + .map(|(index, id)| { + let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + exchange: Some(exchange), + .. + }, + .. + }) = p.elements[id].shared() + else { + panic!("Demo legacy sequence should contain exchange occurrences"); + }; + format!("{}. {}", index + 1, p.relations[exchange].name) + }) + .collect(); + assert!(!expected.is_empty()); + let scene = Scene::sequence(&p, owner.id, owner.id); + let actual: Vec<_> = scene + .lines + .iter() + .filter(|line| !line.label.is_empty()) + .map(|line| line.label.clone()) + .collect(); + assert_eq!( + actual, expected, + "Legacy rendering must preserve the model's ordered exchange path" + ); + } + } + #[test] + fn selecting_state_transition_keeps_its_behavior_context() { + let p = Project::demo(); + for r in p + .relations + .values() + .filter(|r| r.kind() == RelationKind::StateTransition) + { + let machine = selected_behavior(&p, r.id).expect("Transition remains in its machine"); + assert_eq!(p.elements[&machine].kind(), Kind::StateMachine); + assert!(in_machine(&p, r.source(), machine)); + assert!(in_machine(&p, r.target(), machine)); + } + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace.rs new file mode 100644 index 00000000..32b81926 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace.rs @@ -0,0 +1,243 @@ +//! Shared presentation and input contract; contains no model commands. +use crate::app::{App, Message}; +use iced::widget::{button, column, container, opaque, row, scrollable, stack, text, tooltip}; +use iced::{Element, Length::Fill, Point, Theme, Vector}; +use oa_core::Id; +mod control; +mod focus; +mod viewport; +pub use control::Control; +pub use viewport::Viewport; + +pub fn keyboard_enabled(app: &crate::app::App) -> bool { + !app.busy + && !app.hosted.visible + && app.workflow.is_none() + && app.file_prompt.is_none() + && !app.delete_pending + && app.pending.is_none() + && app.model_menu.is_none() + && !app.reference_input_active() +} + +pub fn controls<'a>( + scope: Scope, + keyboard: bool, + scale: f32, + details: Message, + focus: Option, + zoom: impl Fn(Option) -> Message, + publish: impl Fn(Event) -> Message + Copy + 'a, +) -> Element<'a, Message> { + let control = |name: &'static str, label: &'static str, message: Option| { + let message = message.map(|message| intent(scope, name, message)); + Control::new( + button(text(label).size(12)) + .padding(6) + .on_press_maybe(message.clone()), + scope, + name, + keyboard, + message, + publish, + ) + }; + row![ + control("Details", "Details", Some(details)), + tooltip(control("Canvas", "Canvas", focus), + container(text("F6: enter/leave canvas controls · Tab: next · Enter/Space: activate · Esc: leave").size(12)).padding(7).style(crate::style::card), + tooltip::Position::Bottom), + control("Fit", "Fit", Some(zoom(None))), + control("ZoomOut", "−", Some(zoom(Some(scale / 1.25)))), + text(format!("{:.0}%", scale * 100.)).size(12), + control("ZoomIn", "+", Some(zoom(Some(scale * 1.25)))), + control("ActualSize", "100%", Some(zoom(Some(1.)))), + ].spacing(4).align_y(iced::Center).into() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Scope { + pub document: Id, + pub owner: Id, + pub revision: u64, + pub epoch: u64, +} +impl Scope { + pub fn current(app: &App) -> Option { + if let Some(context) = app.scenario_context() { + Some(Self { + document: context.document, + owner: context.scenario, + revision: context.revision, + epoch: app.camera_epoch, + }) + } else { + let context = app.topology_context()?; + Some(Self { + document: context.document, + owner: context.owner, + revision: context.revision, + epoch: app.camera_epoch, + }) + } + } + pub fn input_owner(app: &App) -> Option { + if keyboard_enabled(app) + && !app.behavior.has_pending() + && !app.draft_dirty() + && !app.source_dirty() + { + Self::current(app) + } else { + None + } + } + + pub fn control_id(self, name: &str) -> iced::advanced::widget::Id { + iced::advanced::widget::Id::from(format!( + "behavior-control:{}:{}:{}:{name}", + self.document, self.owner, self.revision + )) + } +} +/// Only a live presentation intent may carry a toolbar action across its own pan. +#[derive(Debug, Clone)] +pub struct Intent { + pub scope: Scope, + pub name: &'static str, + pub message: Box, +} +pub fn intent(scope: Scope, name: &'static str, message: Message) -> Message { + Message::BehaviorControl(Intent { + scope, + name, + message: Box::new(message), + }) +} + +#[derive(Debug, Clone, Copy)] +pub enum Event { + CancelGesture, + Pan(Vector), + Zoom { factor: f32, at: Point, base: f32 }, + Focus, + Traverse(bool), + ControlFocus(&'static str), + ToggleFocus, + Leave, +} + +pub use focus::{focus_task, root as focus_root}; +/// Fold relative input in event order. The base scale is used only on leaving Fit. +pub fn change(pan: &mut Vector, zoom: &mut Option, fit: &mut bool, event: Event) -> bool { + match event { + Event::Pan(delta) if delta.x.is_finite() && delta.y.is_finite() => { + let next = *pan + delta; + if next.x.is_finite() && next.y.is_finite() && next != *pan { + *pan = next; + true + } else { + false + } + } + Event::Zoom { factor, at, base } + if factor.is_finite() + && factor > 0. + && base.is_finite() + && base > 0. + && at.x.is_finite() + && at.y.is_finite() => + { + let old = if *fit { base } else { zoom.unwrap_or(base) }; + let next = (old * factor).clamp(0.05, 3.); + if old == next { + return false; + } + let anchor = Vector::new(at.x, at.y); + let next_pan = anchor - (anchor - *pan) * (next / old); + if !next_pan.x.is_finite() || !next_pan.y.is_finite() { + return false; + } + *pan = next_pan; + *zoom = Some(next); + *fit = false; + true + } + _ => false, + } +} + +/// Constant-width labelled palette. Arming a tool never reallocates the canvas. +pub fn palette<'a>( + scope: Scope, + keyboard: bool, + publish: impl Fn(Event) -> Message + Copy + 'a, + entries: impl IntoIterator)>, +) -> Element<'a, Message> { + let mut tools = column![].spacing(3); + for (glyph, label, selected, message) in entries { + let message = message.map(|message| intent(scope, glyph, message)); + tools = tools.push(tooltip( + container(Control::new( + button(text(glyph).size(12)) + .width(34) + .height(30) + .padding(3) + .style(if selected { + button::primary + } else { + crate::style::nav + }) + .on_press_maybe(message.clone()), + scope, + glyph, + keyboard, + message, + publish, + )) + .id(format!("behavior-tool:{glyph}")), + container(text(label).size(12)) + .padding(7) + .style(crate::style::card), + tooltip::Position::Left, + )); + } + container(scrollable(tools).id(scope.control_id("palette-scroll"))) + .width(40) + .height(Fill) + .padding(3) + .style(crate::style::panel) + .into() +} + +/// Header and palette are permanent. Details/editors overlay the same viewport. +pub fn host<'a, Message: 'a, R: iced::advanced::text::Renderer + 'a>( + header: Element<'a, Message, Theme, R>, + palette: Element<'a, Message, Theme, R>, + canvas: Element<'a, Message, Theme, R>, + editor: Option>, +) -> Element<'a, Message, Theme, R> { + let mut body = stack![canvas]; + if let Some(editor) = editor { + body = body.push( + container(opaque( + container(editor) + .width(300) + .height(Fill) + .style(crate::style::card), + )) + .align_right(Fill) + .height(Fill) + .padding(8), + ); + } + column![header, row![body, palette].height(Fill)] + .spacing(6) + .padding(8) + .width(Fill) + .height(Fill) + .into() +} + +#[cfg(test)] +pub(crate) mod tests; diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/control.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/control.rs new file mode 100644 index 00000000..6e199cca --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/control.rs @@ -0,0 +1,258 @@ +//! A real focusable keyboard target around the existing styled pointer button. +use super::{Event as CameraEvent, Scope, focus::Member}; +use iced::advanced::{ + Clipboard, Layout, Shell, Widget, layout, overlay, renderer, + widget::{Operation, Tree, operation, tree}, +}; +use iced::{Border, Element, Event, Length, Rectangle, Size, Theme, Vector, keyboard, mouse}; + +#[derive(Default)] +struct Input { + identity: Option, + focused: bool, +} +impl operation::Focusable for Input { + fn is_focused(&self) -> bool { + self.focused + } + fn focus(&mut self) { + self.focused = true; + } + fn unfocus(&mut self) { + self.focused = false; + } +} +pub struct Control<'a, Message, R = iced::Renderer> +where + R: renderer::Renderer, +{ + content: Element<'a, Message, Theme, R>, + scope: Scope, + name: &'static str, + enabled: bool, + activate: Option, + publish: Box Message + 'a>, +} +impl<'a, Message, R: renderer::Renderer> Control<'a, Message, R> { + pub fn new( + content: impl Into>, + scope: Scope, + name: &'static str, + enabled: bool, + activate: Option, + publish: impl Fn(CameraEvent) -> Message + 'a, + ) -> Self { + Self { + content: content.into(), + scope, + name, + enabled: enabled && activate.is_some(), + activate, + publish: Box::new(publish), + } + } +} +impl Widget for Control<'_, Message, R> { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + fn state(&self) -> tree::State { + tree::State::new(Input { + identity: Some(self.scope.control_id(self.name)), + focused: false, + }) + } + fn children(&self) -> Vec { + vec![Tree::new(self.content.as_widget())] + } + fn diff(&self, tree: &mut Tree) { + let state = tree.state.downcast_mut::(); + let identity = self.scope.control_id(self.name); + if state.identity.as_ref() != Some(&identity) || !self.enabled { + *state = Input { + identity: Some(identity), + focused: false, + }; + } + tree.diff_children(std::slice::from_ref(&self.content)); + } + fn size(&self) -> Size { + self.content.as_widget().size() + } + fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node { + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + } + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &R, + operation: &mut dyn Operation, + ) { + let id = self.scope.control_id(self.name); + operation.container(Some(&id), layout.bounds()); + if self.enabled { + operation.custom(Some(&id), layout.bounds(), &mut Member(self.scope)); + operation.focusable( + Some(&id), + layout.bounds(), + tree.state.downcast_mut::(), + ); + } + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + }); + } + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &R, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + if shell.is_event_captured() { + return; + } + let state = tree.state.downcast_mut::(); + if self.enabled { + if let Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers, + repeat, + .. + }) = event + && state.focused + { + use keyboard::{Key, key::Named}; + let action = match key.as_ref() { + Key::Named(Named::Tab) => { + Some((self.publish)(CameraEvent::Traverse(modifiers.shift()))) + } + Key::Named(Named::Escape) => Some((self.publish)(CameraEvent::Leave)), + Key::Named(Named::F6) if modifiers.is_empty() && !repeat => { + Some((self.publish)(CameraEvent::ToggleFocus)) + } + Key::Named(Named::Enter | Named::Space) if modifiers.is_empty() && !repeat => { + self.activate.clone() + } + _ => None, + }; + if let Some(action) = action { + shell.publish(action); + shell.capture_event(); + shell.request_redraw(); + return; + } + } + if matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) { + if !layout + .bounds() + .intersection(viewport) + .is_some_and(|bounds| cursor.is_over(bounds)) + { + state.focused = false; + } + } + } + if matches!(event, Event::Window(iced::window::Event::Unfocused)) { + state.focused = false; + } + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + fn draw( + &self, + tree: &Tree, + renderer: &mut R, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + if self.enabled && tree.state.downcast_ref::().focused { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border: Border { + color: crate::style::INK, + width: 2., + ..Default::default() + }, + ..Default::default() + }, + iced::Color::TRANSPARENT, + ); + } + } + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &R, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + fn overlay<'a>( + &'a mut self, + tree: &'a mut Tree, + layout: Layout<'a>, + renderer: &R, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} +impl<'a, Message: Clone + 'a, R: renderer::Renderer + 'a> From> + for Element<'a, Message, Theme, R> +{ + fn from(control: Control<'a, Message, R>) -> Self { + Element::new(control) + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/focus.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/focus.rs new file mode 100644 index 00000000..52ce9dd2 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/focus.rs @@ -0,0 +1,293 @@ +//! Explicit canvas entry; ordinary initial Tab continues to belong to Inspector. +use super::{Event, Scope}; +use iced::advanced::widget::{Id, Operation, Tree, operation}; +use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, overlay, renderer}; +use iced::{Element, Event as InputEvent, Length, Rectangle, Size, Theme, Vector, mouse}; + +pub(super) struct Member(pub Scope); + +pub fn focus_task(scope: Scope, event: Event) -> iced::Task { + iced::advanced::widget::operate(Collect { + scope, + valid: false, + event, + members: vec![], + focused: None, + palette: None, + }) +} +struct Collect { + scope: Scope, + valid: bool, + event: Event, + members: Vec<(Id, Rectangle)>, + focused: Option, + palette: Option<(Rectangle, Rectangle)>, +} +impl Operation for Collect { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) { + visit(self); + } + fn custom(&mut self, id: Option<&Id>, bounds: Rectangle, value: &mut dyn std::any::Any) { + if let Some(owner) = value.downcast_ref::() { + self.valid = owner.0 == Some(self.scope); + } + if self.valid + && value + .downcast_ref::() + .is_some_and(|m| m.0 == self.scope) + && let Some(id) = id + { + self.members.push((id.clone(), bounds)); + } + } + fn focusable(&mut self, id: Option<&Id>, _: Rectangle, value: &mut dyn operation::Focusable) { + if value.is_focused() { + self.focused = self + .members + .iter() + .position(|(member, _)| Some(member) == id) + .or(self.focused); + } + } + fn scrollable( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + content: Rectangle, + _: Vector, + _: &mut dyn operation::Scrollable, + ) { + if id == Some(&self.scope.control_id("palette-scroll")) { + self.palette = Some((bounds, content)); + } + } + fn finish(&self) -> operation::Outcome { + let canvas = self.scope.control_id("viewport"); + // An enabled viewport witnesses the live lifetime. Hidden/dirty/modal + // owners expose no members; a delayed request cannot clear their focus. + if !self.valid || !self.members.iter().any(|(id, _)| *id == canvas) { + return operation::Outcome::None; + } + let target = match self.event { + Event::Focus => Some(canvas), + Event::ControlFocus(name) => Some(self.scope.control_id(name)), + Event::ToggleFocus if self.focused.is_none() => Some(canvas), + Event::ToggleFocus | Event::Leave if self.focused.is_some() => None, + Event::Traverse(back) => { + let Some(current) = self.focused else { + return operation::Outcome::None; + }; + let next = if back { + (current + self.members.len() - 1) % self.members.len() + } else { + (current + 1) % self.members.len() + }; + Some(self.members[next].0.clone()) + } + _ => return operation::Outcome::None, + }; + if target + .as_ref() + .is_some_and(|target| !self.members.iter().any(|(id, _)| id == target)) + { + return operation::Outcome::None; + } + let reveal = target.as_ref().and_then(|target| { + let (_, field) = self.members.iter().find(|(id, _)| id == target)?; + let (bounds, content) = self.palette?; + // Only the palette has member controls inside this scrollable. + (field.x >= bounds.x && field.x < bounds.x + bounds.width).then(|| { + (field.center_y() - bounds.y - bounds.height / 2.) + .clamp(0., (content.height - bounds.height).max(0.)) + }) + }); + operation::Outcome::Chain(Box::new(Apply { + scope: self.scope, + valid: false, + target, + members: self.members.iter().map(|(id, _)| id.clone()).collect(), + palette: self.scope.control_id("palette-scroll"), + reveal, + })) + } +} +struct Apply { + scope: Scope, + valid: bool, + target: Option, + members: Vec, + palette: Id, + reveal: Option, +} +impl Operation for Apply { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn Operation)) { + visit(self); + } + fn custom(&mut self, _: Option<&Id>, _: Rectangle, value: &mut dyn std::any::Any) { + if let Some(owner) = value.downcast_ref::() { + self.valid = owner.0 == Some(self.scope); + } + } + fn focusable(&mut self, id: Option<&Id>, _: Rectangle, state: &mut dyn operation::Focusable) { + if !self.valid { + return; + } + if self + .target + .as_ref() + .is_some_and(|target| Some(target) == id) + { + state.focus(); + } else if self.target.is_some() || id.is_some_and(|id| self.members.contains(id)) { + state.unfocus(); + } + } + fn scrollable( + &mut self, + id: Option<&Id>, + _: Rectangle, + _: Rectangle, + _: Vector, + state: &mut dyn operation::Scrollable, + ) { + if self.valid + && id == Some(&self.palette) + && let Some(y) = self.reveal + { + state.scroll_to(operation::scrollable::AbsoluteOffset { + x: None, + y: Some(y), + }); + } + } +} + +// Every Collect/Apply pass observes current ownership before touching any field. +struct Owner(Option); + +struct Root<'a, Message, R: renderer::Renderer> { + owner: Option, + content: Element<'a, Message, Theme, R>, +} + +pub fn root<'a, Message: 'a, R: renderer::Renderer + 'a>( + content: impl Into>, + owner: Option, +) -> Element<'a, Message, Theme, R> { + Element::new(Root { + owner, + content: content.into(), + }) +} + +impl Widget for Root<'_, Message, R> { + fn children(&self) -> Vec { + vec![Tree::new(self.content.as_widget())] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node { + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &R, + operation: &mut dyn Operation, + ) { + operation.custom(None, layout.bounds(), &mut Owner(self.owner)); + self.content + .as_widget_mut() + .operate(&mut tree.children[0], layout, renderer, operation); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &InputEvent, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &R, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut R, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &R, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &R, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/tests.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/tests.rs new file mode 100644 index 00000000..ba9f77cc --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/tests.rs @@ -0,0 +1,757 @@ +use super::*; +use crate::app::{App, Message}; +use iced::advanced::{Layout, Shell, layout, widget}; +use iced::futures::{ + StreamExt, + stream::{BoxStream, SelectAll}, +}; +use iced::{Rectangle, Size, mouse}; +use iced_runtime::{ + Action, + user_interface::{Cache, UserInterface}, +}; +use std::collections::HashMap; + +type Pending = SelectAll>>; + +// iced_winit drains messages while collecting immediately ready actions. A +// pending Task stream survives until its widget operation sends its result. +fn enqueue(task: iced::Task, ready: &mut Vec>, pending: &mut Pending) { + if let Some(mut stream) = iced_runtime::task::into_stream(task) { + let waker = iced::futures::task::noop_waker(); + let mut context = std::task::Context::from_waker(&waker); + loop { + match stream.poll_next_unpin(&mut context) { + std::task::Poll::Ready(Some(action)) => ready.push(action), + std::task::Poll::Ready(None) => break, + std::task::Poll::Pending => { + pending.push(stream); + break; + } + } + } + } +} + +#[derive(Default)] +pub(crate) struct Geometry { + pub containers: HashMap, + pub focused: Vec<(Option, Rectangle, bool)>, + pub text_inputs: Vec, + pub scrolls: HashMap, +} +impl widget::Operation for Geometry { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { + visit(self); + } + fn focusable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + state: &mut dyn widget::operation::Focusable, + ) { + self.focused.push((id.cloned(), bounds, state.is_focused())); + } + fn text_input( + &mut self, + _: Option<&widget::Id>, + bounds: Rectangle, + _: &mut dyn widget::operation::TextInput, + ) { + self.text_inputs.push(bounds); + } + fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) { + if let Some(id) = id { + self.containers.insert(id.clone(), bounds); + } + } + fn scrollable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _: Rectangle, + offset: Vector, + _: &mut dyn widget::operation::Scrollable, + ) { + if let Some(id) = id { + self.scrolls.insert(id.clone(), (bounds, offset)); + } + } +} +impl Geometry { + pub fn bounds(&self, id: &str) -> Rectangle { + self.containers[&widget::Id::from(id.to_owned())] + } +} + +/// Mount the actual App root, production widgets and renderer. Events are +/// delivered in order with Iced's final-batch cursor, then fed through App::update. +/// This deliberately does not seed any private canvas gesture state. +pub(crate) struct Mounted { + pub renderer: iced::Renderer, + cache: Cache, + pub operations: usize, +} +impl Mounted { + pub async fn unshown() -> Self { + use iced::advanced::renderer::Headless; + let renderer = ::new(iced::Font::DEFAULT, 16.into(), None) + .await + .expect("production mounted tests require the supported WGPU headless renderer"); + Self { + renderer, + cache: Cache::new(), + operations: 0, + } + } + pub async fn new(app: &mut App) -> Self { + let mut mounted = Self::unshown().await; + mounted.settle(app).await; + mounted + } + pub fn geometry(&mut self, app: &App) -> Geometry { + let mut geometry = Geometry::default(); + self.operate(app, &mut geometry); + geometry + } + /// One operation pass, including every nested overlay, via installed Iced. + pub fn operate(&mut self, app: &App, operation: &mut dyn widget::Operation) { + let mut ui = UserInterface::build( + app.view(), + app.window_size, + std::mem::take(&mut self.cache), + &mut self.renderer, + ); + ui.operate(&self.renderer, operation); + self.cache = ui.into_cache(); + } + pub async fn settle(&mut self, app: &mut App) { + for _ in 0..3 { + self.batch( + app, + &[iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + ))], + mouse::Cursor::Unavailable, + ) + .await; + } + } + pub async fn batch( + &mut self, + app: &mut App, + events: &[iced::Event], + cursor: mouse::Cursor, + ) -> Vec { + let messages = self.input(app, events, cursor); + let mut ready = vec![]; + let mut pending = Pending::new(); + for message in messages.iter().cloned() { + enqueue(app.update(message), &mut ready, &mut pending); + } + self.drain(app, ready, pending).await; + messages + } + pub fn input( + &mut self, + app: &App, + events: &[iced::Event], + cursor: mouse::Cursor, + ) -> Vec { + let mut messages = vec![]; + let mut ui = UserInterface::build( + app.view(), + app.window_size, + std::mem::take(&mut self.cache), + &mut self.renderer, + ); + let _ = ui.update( + events, + cursor, + &mut self.renderer, + &mut iced::advanced::clipboard::Null, + &mut messages, + ); + self.cache = ui.into_cache(); + messages + } + pub async fn perform(&mut self, app: &mut App, task: iced::Task) { + let mut ready = vec![]; + let mut pending = Pending::new(); + enqueue(task, &mut ready, &mut pending); + self.drain(app, ready, pending).await; + } + async fn drain( + &mut self, + app: &mut App, + mut ready: Vec>, + mut pending: Pending, + ) { + loop { + let mut outputs = vec![]; + if !ready.is_empty() { + // Build only after all App messages in this cycle have run. + let mut ui = UserInterface::build( + app.view(), + app.window_size, + std::mem::take(&mut self.cache), + &mut self.renderer, + ); + for action in ready.drain(..) { + match action { + Action::Output(message) => outputs.push(message), + Action::Widget(mut operation) => { + self.operations += 1; + loop { + ui.operate(&self.renderer, &mut *operation); + match operation.finish() { + widget::operation::Outcome::Chain(next) => operation = next, + widget::operation::Outcome::None + | widget::operation::Outcome::Some(()) => break, + } + } + } + action => { + panic!("Unexpected native action in mounted App harness: {action:?}") + } + } + } + self.cache = ui.into_cache(); + } + for message in outputs { + enqueue(app.update(message), &mut ready, &mut pending); + } + if !ready.is_empty() { + continue; + } + let Some(action) = pending.next().await else { + break; + }; + ready.push(action); + let waker = iced::futures::task::noop_waker(); + let mut context = std::task::Context::from_waker(&waker); + while let std::task::Poll::Ready(Some(action)) = pending.poll_next_unpin(&mut context) { + ready.push(action); + } + } + } + pub fn pixels(&mut self, app: &App) -> Vec { + use iced::advanced::renderer::Headless; + let mut ui = UserInterface::build( + app.view(), + app.window_size, + std::mem::take(&mut self.cache), + &mut self.renderer, + ); + // update initializes the nested overlay layout used by draw. + let mut messages = vec![]; + let _ = ui.update( + &[], + mouse::Cursor::Unavailable, + &mut self.renderer, + &mut iced::advanced::clipboard::Null, + &mut messages, + ); + assert!( + messages.is_empty(), + "An empty draw preparation must not drop App messages" + ); + ui.draw( + &mut self.renderer, + &crate::style::theme(), + &iced::advanced::renderer::Style { + text_color: crate::style::INK, + }, + mouse::Cursor::Unavailable, + ); + self.cache = ui.into_cache(); + Headless::screenshot( + &mut self.renderer, + Size::new(app.window_size.width as u32, app.window_size.height as u32), + 1., + crate::style::BG, + ) + } + pub async fn click(&mut self, app: &mut App, point: Point) -> Vec { + self.batch( + app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + ], + mouse::Cursor::Available(point), + ) + .await + } + pub async fn pan(&mut self, app: &mut App, start: Point, end: Point) -> Vec { + self.batch( + app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: start }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)), + iced::Event::Mouse(mouse::Event::CursorMoved { position: end }), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)), + ], + mouse::Cursor::Available(end), + ) + .await + } + pub fn scroll(&mut self, app: &App, id: &str, offset: f32) { + let mut operation = widget::operation::scrollable::scroll_to::<()>( + widget::Id::from(id.to_owned()), + widget::operation::scrollable::AbsoluteOffset { + x: None, + y: Some(offset), + }, + ); + self.operate(app, &mut operation); + } +} + +pub(crate) fn key(named: iced::keyboard::key::Named) -> iced::Event { + use iced::keyboard::{self, Key}; + iced::Event::Keyboard(keyboard::Event::KeyPressed { + key: Key::Named(named), + modified_key: Key::Named(named), + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::empty(), + text: None, + repeat: false, + }) +} + +#[test] +fn relative_camera_zoom_keeps_the_cursor_world_point_and_rejects_nonfinite_inputs() { + for scale in [0.05, 0.5, 1., 2.] { + let mut pan = Vector::new(-130., 75.); + let mut zoom = Some(scale); + let mut fit = false; + let at = Point::new(171., 93.); + let world = (at - Point::ORIGIN - pan) / scale; + change( + &mut pan, + &mut zoom, + &mut fit, + Event::Zoom { + factor: 1.25, + at, + base: scale, + }, + ); + let after = (at - Point::ORIGIN - pan) / zoom.unwrap(); + assert!((after.x - world.x).abs() < 0.001 && (after.y - world.y).abs() < 0.001); + let before = (pan, zoom, fit); + for input in [ + Event::Pan(Vector::new(f32::INFINITY, 0.)), + Event::Zoom { + factor: f32::NAN, + at, + base: scale, + }, + Event::Zoom { + factor: 0., + at, + base: scale, + }, + ] { + assert!(!change(&mut pan, &mut zoom, &mut fit, input)); + assert_eq!((pan, zoom, fit), before); + } + } +} + +#[test] +fn viewport_ordered_batch_uses_each_event_position_and_cancels_before_camera_motion() { + use iced::widget::Space; + let scope = Scope { + document: Id::new_v4(), + owner: Id::new_v4(), + revision: 2, + epoch: 0, + }; + let mut view: Element<'_, Event, Theme, ()> = Viewport::new( + Space::new().width(Fill).height(Fill), + scope, + 1., + true, + |e| e, + ) + .into(); + let mut tree = widget::Tree::new(view.as_widget()); + let node = view + .as_widget_mut() + .layout( + &mut tree, + &(), + &layout::Limits::new(Size::ZERO, Size::new(400., 300.)), + ) + .move_to(Point::new(30., 80.)); + let start = Point::new(90., 160.); + let middle = Point::new(125., 175.); + let end = Point::new(145., 185.); + let mut messages = vec![]; + for event in [ + mouse::Event::CursorMoved { position: start }, + mouse::Event::ButtonPressed(mouse::Button::Middle), + mouse::Event::CursorMoved { position: middle }, + mouse::Event::CursorMoved { position: end }, + mouse::Event::ButtonReleased(mouse::Button::Middle), + ] { + view.as_widget_mut().update( + &mut tree, + &iced::Event::Mouse(event), + Layout::new(&node), + mouse::Cursor::Available(end), + &(), + &mut iced::advanced::clipboard::Null, + &mut Shell::new(&mut messages), + &Rectangle::with_size(Size::new(700., 600.)), + ); + } + assert!(matches!(messages.first(), Some(Event::CancelGesture))); + let total = messages + .iter() + .fold(Vector::ZERO, |total, event| match event { + Event::Pan(delta) => total + *delta, + _ => total, + }); + assert_eq!(total, end - start); +} + +pub(crate) fn scope(app: &App) -> Scope { + if let Some(context) = app.scenario_context() { + Scope { + document: context.document, + owner: context.scenario, + revision: context.revision, + epoch: app.camera_epoch, + } + } else { + let context = app.topology_context().expect("mounted behavior owner"); + Scope { + document: context.document, + owner: context.owner, + revision: context.revision, + epoch: app.camera_epoch, + } + } +} +pub(crate) fn focused(mounted: &mut Mounted, app: &App, name: &str) -> bool { + let id = scope(app).control_id(name); + mounted + .geometry(app) + .focused + .iter() + .any(|(member, _, focused)| member.as_ref() == Some(&id) && *focused) +} +pub(crate) async fn tab_to(mounted: &mut Mounted, app: &mut App, name: &str) { + for _ in 0..32 { + if focused(mounted, app, name) { + return; + } + mounted + .batch( + app, + &[key(iced::keyboard::key::Named::Tab)], + mouse::Cursor::Unavailable, + ) + .await; + } + panic!("Keyboard traversal did not reach {name}"); +} +pub(crate) fn camera_delta(messages: &[Message]) -> Message { + messages.iter().find(|m| matches!(m, + Message::ScenarioCanvas(crate::behavior::scenario::CanvasEvent::Camera(_, _, Event::Pan(delta))) + | Message::TopologyCanvas(crate::behavior::topology::CanvasEvent::Camera(_, _, Event::Pan(delta))) if *delta != Vector::ZERO + )).expect("actual nonzero camera delta from a mounted gesture").clone() +} +pub(crate) async fn press_middle(mounted: &mut Mounted, app: &mut App, point: Point) { + mounted + .batch( + app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)), + ], + mouse::Cursor::Available(point), + ) + .await; +} +pub(crate) async fn release_middle( + mounted: &mut Mounted, + app: &mut App, + point: Point, +) -> Vec { + mounted + .batch( + app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)), + ], + mouse::Cursor::Available(point), + ) + .await +} +pub(crate) fn assert_drawn_focus(before: &[u8], after: &[u8], size: Size, bounds: Rectangle) { + let width = size.width as usize; + assert_eq!(before.len(), width * size.height as usize * 4); + assert_eq!(after.len(), before.len()); + let mut changed = 0; + for y in bounds.y.ceil() as usize..(bounds.y + bounds.height).floor() as usize { + for x in bounds.x.ceil() as usize..(bounds.x + bounds.width).floor() as usize { + if x as f32 - bounds.x > 2. + && bounds.x + bounds.width - x as f32 > 2. + && y as f32 - bounds.y > 2. + && bounds.y + bounds.height - y as f32 > 2. + { + continue; + } + let start = (y * width + x) * 4; + if before[start..start + 3] != after[start..start + 3] { + changed += 1; + } + } + } + assert!( + changed >= bounds.width.min(bounds.height) as usize, + "Focus must change visible border pixels, changed={changed}, bounds={bounds:?}" + ); +} + +pub(crate) fn character(value: &str) -> iced::Event { + let mut event = key(iced::keyboard::key::Named::Space); + if let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key, + modified_key, + text, + .. + }) = &mut event + { + *key = iced::keyboard::Key::Character(value.into()); + *modified_key = key.clone(); + *text = Some(value.into()); + } + event +} + +/// Inspect the action without stripping the lifetime from retained packets. +pub(crate) fn intent_action(message: &Message) -> &Message { + match message { + Message::BehaviorControl(intent) => &intent.message, + message => message, + } +} + +/// Deliberately retain a returned Chain to test the stronger ownership invariant. +/// Native Iced runs Chain passes synchronously; this is not a native interleaving. +pub(crate) async fn retained_focus_apply( + mounted: &mut Mounted, + app: &mut App, +) -> Box { + let entry = mounted.input( + app, + &[key(iced::keyboard::key::Named::F6)], + mouse::Cursor::Unavailable, + ); + assert_eq!( + entry.len(), + 1, + "actual initial F6 must emit one canvas focus request" + ); + let mut stream = iced_runtime::task::into_stream(app.update(entry[0].clone())) + .expect("F6 returns a widget Task"); + let Some(Action::Widget(mut operation)) = stream.next().await else { + panic!("F6 first action must be Collect"); + }; + mounted.operate(app, &mut *operation); + match operation.finish() { + widget::operation::Outcome::Chain(apply) => apply, + _ => panic!("Live Collect must produce its guarded Apply pass"), + } +} + +pub(crate) async fn overflow_last_tool(mounted: &mut Mounted, app: &mut App, last: &'static str) { + use iced::keyboard::key::Named; + let geometry = mounted.geometry(app); + let last_bounds = geometry.containers[&scope(app).control_id(last)]; + let (rail, offset) = geometry.scrolls[&scope(app).control_id("palette-scroll")]; + assert_eq!(offset.y, 0.); + assert!( + last_bounds.y >= rail.y + rail.height, + "last tool starts fully below the overflowing palette: tool={last_bounds:?}, rail={rail:?}" + ); + mounted + .batch(app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + tab_to(mounted, app, last).await; + let geometry = mounted.geometry(app); + let mut visible = geometry.containers[&scope(app).control_id(last)]; + let (rail, offset) = geometry.scrolls[&scope(app).control_id("palette-scroll")]; + assert!(offset.y > 0., "keyboard traversal must scroll the palette"); + visible.x -= offset.x; + visible.y -= offset.y; + assert!( + visible.y >= rail.y && visible.y + visible.height <= rail.y + rail.height + 0.5, + "the entire last tool is revealed: tool={visible:?}, rail={rail:?}" + ); + let messages = mounted + .batch(app, &[key(Named::Enter)], mouse::Cursor::Unavailable) + .await; + assert!( + messages.iter().any( + |message| matches!(message, Message::BehaviorControl(intent) if intent.name == last) + ), + "the revealed last tool is keyboard activated" + ); + assert!(focused(mounted, app, last)); + let on = mounted.pixels(app); + mounted + .batch(app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + let off = mounted.pixels(app); + assert_drawn_focus(&off, &on, app.window_size, visible); +} + +pub(crate) async fn retained_toolbar_boundaries( + mounted: &mut Mounted, + app: &mut App, + tool: &'static str, +) { + use iced::keyboard::key::Named; + for boundary in ["Chooser", "Dirty", "Picker"] { + let mut packets = vec![]; + for name in ["Fit", tool, "Details", "Panels", "Canvas"] { + packets.push(capture_toolbar(mounted, app, name)); + } + let lifetime = app.camera_epoch; + let epoch = app.canvas_epoch; + let process = app.behavior.topology.camera_boundary(); + let scenario = app.behavior.scenario.camera_boundary(); + match boundary { + "Chooser" => { + let draft = app.draft.as_ref().unwrap(); + let id = crate::forms::reference_chooser::trigger::field_id( + draft.reference_generation(), + "/parent", + ); + let bounds = mounted + .geometry(app) + .focused + .iter() + .find(|(field, _, _)| field.as_ref() == Some(&id)) + .expect("real Inspector parent reference trigger") + .1; + let request = draft.reference_open(app.project(), "/parent", bounds); + let task = app.update(Message::FieldReferenceOpen(request)); + mounted.perform(app, task).await; + assert!(app.reference_input_active()); + mounted + .batch(app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + assert!(!app.reference_input_active()); + } + "Dirty" => { + let task = app.update(Message::Field( + "/name".into(), + "Unsaved camera boundary".into(), + )); + mounted.perform(app, task).await; + assert!(app.draft_dirty()); + // Every retained packet is also rejected while the owner is dirty. + for packet in packets.iter().cloned() { + let task = app.update(packet); + mounted.perform(app, task).await; + } + assert!(app.draft_dirty()); + let task = app.update(Message::RevertProperties); + mounted.perform(app, task).await; + assert!(!app.draft_dirty()); + } + "Picker" => { + // Public transitions, with cancellation injected at the external + // picker boundary. Never poll the native dialog future here. + let picker = app.update(Message::Save(true)); + if cfg!(target_os = "linux") { + assert!(app.file_prompt.is_some()); + mounted.perform(app, picker).await; + let task = app.update(Message::CancelFile); + mounted.perform(app, task).await; + } else { + assert!(app.busy, "Save As enters the real native-picker busy state"); + drop(picker); + for packet in packets.iter().cloned() { + let task = app.update(packet); + mounted.perform(app, task).await; + } + let task = app.update(Message::SavePath(None)); + mounted.perform(app, task).await; + assert!(!app.busy); + } + } + _ => unreachable!(), + } + assert_ne!( + app.camera_epoch, lifetime, + "{boundary} changes the presentation lifetime" + ); + assert_eq!( + app.canvas_epoch, epoch, + "{boundary} cycle leaves semantic context unchanged" + ); + let focus_before = mounted + .geometry(app) + .focused + .into_iter() + .filter(|(_, _, value)| *value) + .collect::>(); + let current_lifetime = app.camera_epoch; + for packet in packets { + let task = app.update(packet); + mounted.perform(app, task).await; + } + assert_eq!( + app.camera_epoch, current_lifetime, + "{boundary} retained intents cannot start a new lifetime" + ); + assert!( + app.behavior.topology.camera_boundary() == process, + "{boundary} retained toolbar packet changed process presentation" + ); + assert!( + app.behavior.scenario.camera_boundary() == scenario, + "{boundary} retained toolbar packet changed scenario presentation" + ); + assert_eq!( + mounted + .geometry(app) + .focused + .into_iter() + .filter(|(_, _, value)| *value) + .collect::>(), + focus_before, + "{boundary} retained toolbar packet stole field focus" + ); + } +} + +pub(crate) fn capture_toolbar(mounted: &mut Mounted, app: &App, name: &'static str) -> Message { + let point = mounted.geometry(app).containers[&scope(app).control_id(name)].center(); + mounted + .input( + app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + ], + mouse::Cursor::Available(point), + ) + .into_iter() + .find(|message| matches!(message, Message::BehaviorControl(intent) if intent.name == name)) + .expect("actual production toolbar packet") +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs new file mode 100644 index 00000000..58d46463 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/canvas_workspace/viewport.rs @@ -0,0 +1,426 @@ +use super::{Event as CameraEvent, Scope}; +use iced::advanced::{ + Clipboard, Layout, Shell, Widget, layout, overlay, renderer, + widget::{Operation, Tree, operation, tree}, +}; +use iced::{Border, Element, Event, Length, Point, Rectangle, Size, Theme, Vector}; +use iced::{keyboard, mouse}; + +#[derive(Default)] +struct Input { + scope: Option, + focused: bool, + pointer: Option, + pan: Option<(mouse::Button, Point)>, + space: bool, + bounds: Option, +} +impl operation::Focusable for Input { + fn is_focused(&self) -> bool { + self.focused + } + fn focus(&mut self) { + self.focused = true; + } + fn unfocus(&mut self) { + self.focused = false; + self.pan = None; + self.space = false; + } +} +pub struct Viewport<'a, Message, R = iced::Renderer> +where + R: renderer::Renderer, +{ + content: Element<'a, Message, Theme, R>, + scope: Scope, + scale: f32, + keyboard: bool, + blank_pan: bool, + publish: Box Message + 'a>, +} +impl<'a, Message, R: renderer::Renderer> Viewport<'a, Message, R> { + pub fn new( + content: impl Into>, + scope: Scope, + scale: f32, + keyboard: bool, + publish: impl Fn(CameraEvent) -> Message + 'a, + ) -> Self { + Self { + content: content.into(), + scope, + scale, + keyboard, + blank_pan: false, + publish: Box::new(publish), + } + } + pub fn pan_background(mut self, enabled: bool) -> Self { + self.blank_pan = enabled; + self + } +} +impl Widget for Viewport<'_, Message, R> { + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + fn state(&self) -> tree::State { + tree::State::new(Input { + scope: Some(self.scope), + ..Default::default() + }) + } + fn children(&self) -> Vec { + vec![Tree::new(self.content.as_widget())] + } + fn diff(&self, tree: &mut Tree) { + let state = tree.state.downcast_mut::(); + if state.scope.is_none_or(|s| { + (s.document, s.owner, s.revision) + != (self.scope.document, self.scope.owner, self.scope.revision) + }) { + *state = Input::default(); + } + if state + .scope + .is_some_and(|scope| scope.epoch != self.scope.epoch) + || !self.keyboard + { + state.pan = None; + state.space = false; + } + if !self.keyboard { + state.focused = false; + } + state.scope = Some(self.scope); + tree.diff_children(std::slice::from_ref(&self.content)); + } + fn size(&self) -> Size { + Size::new(Length::Fill, Length::Fill) + } + fn layout(&mut self, tree: &mut Tree, renderer: &R, limits: &layout::Limits) -> layout::Node { + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + } + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &R, + operation: &mut dyn Operation, + ) { + if self.keyboard { + let id = self.scope.control_id("viewport"); + operation.custom( + Some(&id), + layout.bounds(), + &mut super::focus::Member(self.scope), + ); + operation.focusable( + Some(&id), + layout.bounds(), + tree.state.downcast_mut::(), + ); + } + self.content + .as_widget_mut() + .operate(&mut tree.children[0], layout, renderer, operation); + } + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &R, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + if shell.is_event_captured() { + return; + } + let state = tree.state.downcast_mut::(); + let changed_bounds = state.bounds.is_some_and(|bounds| bounds != layout.bounds()); + state.bounds = Some(layout.bounds()); + if changed_bounds || !self.keyboard { + state.pan = None; + state.space = false; + } + if !self.keyboard { + state.focused = false; + } + if let Event::Mouse(mouse::Event::CursorMoved { position }) = event { + state.pointer = Some(*position); + } + let pointer = match event { + Event::Mouse( + mouse::Event::ButtonPressed(_) + | mouse::Event::ButtonReleased(_) + | mouse::Event::WheelScrolled { .. }, + ) => state.pointer.or_else(|| cursor.position()), + Event::Mouse(mouse::Event::CursorMoved { position }) => Some(*position), + _ => cursor.position().or(state.pointer), + }; + let visible = layout.bounds().intersection(viewport); + let inside = pointer.is_some_and(|p| visible.is_some_and(|v| v.contains(p))) + && cursor.position().is_some(); + if !self.keyboard { + state.space = false; + } + let mut cancel = changed_bounds; + let mut camera = None; + let mut capture = false; + match event { + Event::Window(iced::window::Event::Unfocused | iced::window::Event::Resized(_)) => { + state.pan = None; + state.space = false; + state.focused = false; + } + Event::Mouse(mouse::Event::CursorLeft) => { + state.pan = None; + state.pointer = None; + } + Event::Mouse(mouse::Event::ButtonPressed(button)) => { + if state.pan.is_some_and(|(held, _)| held != *button) || !inside { + state.pan = None; + state.space = false; + cancel = true; + } + state.focused = inside && self.keyboard; + if inside + && self.keyboard + && (*button == mouse::Button::Middle + || (*button == mouse::Button::Left && state.space)) + { + state.pan = pointer.map(|p| (*button, p)); + cancel = true; + camera = Some(CameraEvent::CancelGesture); + capture = true; + } + } + Event::Mouse(mouse::Event::CursorMoved { position }) => { + if let Some((button, previous)) = state.pan { + state.pan = Some((button, *position)); + camera = Some(CameraEvent::Pan(*position - previous)); + capture = true; + } + } + Event::Mouse(mouse::Event::ButtonReleased(button)) => { + if let Some((held, previous)) = state.pan + && held == *button + { + state.pan = None; + camera = pointer + .filter(|_| inside) + .map(|p| CameraEvent::Pan(p - previous)); + capture = true; + } + } + Event::Mouse(mouse::Event::WheelScrolled { delta }) if inside && state.focused => { + state.pan = None; + cancel = true; + let y = match delta { + mouse::ScrollDelta::Lines { y, .. } => *y, + mouse::ScrollDelta::Pixels { y, .. } => *y / 40., + }; + if y != 0. { + let at = pointer.unwrap() - Vector::new(layout.bounds().x, layout.bounds().y); + camera = Some(CameraEvent::Zoom { + factor: 1.1_f32.powf(y.clamp(-8., 8.)), + at, + base: self.scale, + }); + capture = true; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(keyboard::key::Named::F6), + modifiers, + repeat: false, + .. + }) if self.keyboard && modifiers.is_empty() => { + camera = Some(CameraEvent::ToggleFocus); + capture = true; + cancel = true; + } + Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) + if state.focused && self.keyboard => + { + use keyboard::{Key, key::Named}; + match key.as_ref() { + Key::Named(Named::Space) => { + state.space = true; + cancel = true; + camera = Some(CameraEvent::CancelGesture); + capture = true; + } + Key::Named(Named::Tab) => { + camera = Some(CameraEvent::Traverse(modifiers.shift())); + capture = true; + } + Key::Named(Named::Escape) => { + state.pan = None; + state.space = false; + camera = Some(CameraEvent::Leave); + capture = true; + } + Key::Named(key) if modifiers.is_empty() => { + let delta = match key { + Named::ArrowLeft => Some(Vector::new(60., 0.)), + Named::ArrowRight => Some(Vector::new(-60., 0.)), + Named::ArrowUp => Some(Vector::new(0., 60.)), + Named::ArrowDown => Some(Vector::new(0., -60.)), + _ => None, + }; + camera = delta.map(CameraEvent::Pan); + capture = camera.is_some(); + cancel = capture; + } + _ => {} + } + } + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) + if *key == keyboard::Key::Named(keyboard::key::Named::Space) => + { + state.space = false; + } + _ => {} + } + if cancel { + self.content.as_widget_mut().update( + &mut tree.children[0], + &Event::Window(iced::window::Event::Unfocused), + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + if let Some(event) = camera { + shell.publish((self.publish)(event)); + } + if capture { + shell.capture_event(); + shell.request_redraw(); + return; + } + if matches!(event, Event::Keyboard(_)) && (!state.focused || !self.keyboard) { + return; + } + let child_cursor = if inside { + pointer.map_or(mouse::Cursor::Unavailable, mouse::Cursor::Available) + } else { + mouse::Cursor::Unavailable + }; + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + child_cursor, + renderer, + clipboard, + shell, + viewport, + ); + // A semantic hit or armed creation tool gets first refusal. In Select, + // an otherwise unused blank press starts the same camera gesture. + if self.blank_pan + && self.keyboard + && inside + && !shell.is_event_captured() + && matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) + { + state.pan = pointer.map(|p| (mouse::Button::Left, p)); + shell.publish((self.publish)(CameraEvent::CancelGesture)); + shell.capture_event(); + shell.request_redraw(); + } + } + fn draw( + &self, + tree: &Tree, + renderer: &mut R, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + renderer.with_layer(layout.bounds(), |renderer| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + }); + if tree.state.downcast_ref::().focused && self.keyboard { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border: Border { + color: crate::style::TEAL, + width: 1., + ..Default::default() + }, + ..Default::default() + }, + iced::Color::TRANSPARENT, + ); + } + } + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &R, + ) -> mouse::Interaction { + if tree.state.downcast_ref::().pan.is_some() { + mouse::Interaction::Grabbing + } else { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + } + fn overlay<'a>( + &'a mut self, + tree: &'a mut Tree, + layout: Layout<'a>, + renderer: &R, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} +impl<'a, Message: 'a, R: renderer::Renderer + 'a> From> + for Element<'a, Message, Theme, R> +{ + fn from(viewport: Viewport<'a, Message, R>) -> Self { + Element::new(viewport) + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario.rs new file mode 100644 index 00000000..ab8bc786 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario.rs @@ -0,0 +1,3957 @@ +//! Staged scenario authoring. UI picks semantic identities; the core validates +//! the complete transaction, including correlation, lifecycle and reuse rules. +use super::topology::{occurrence, unique_batch}; +use super::*; +use iced::widget::{Column, column, text_input}; +use std::{cell::RefCell, collections::BTreeSet}; +pub(crate) mod focus; +mod workspace; +pub(crate) use workspace::NAME_INPUT; +pub use workspace::{Shelf, view}; + +#[cfg(test)] +#[path = "scenario_operation_tests.rs"] +mod operation_tests; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum Tool { + Lifeline, + Message, + Reuse, + Action, + Observation, + Fragment, +} +impl Tool { + pub(super) fn canvas_occurrence(self) -> bool { + matches!( + self, + Self::Reuse | Self::Action | Self::Observation | Self::Fragment + ) + } + pub(super) fn uses_lifeline(self) -> bool { + matches!(self, Self::Action | Self::Observation) + } + fn label(self) -> &'static str { + match self { + Self::Lifeline => "Add lifeline", + Self::Message => "Add message", + Self::Reuse => "Reuse scenario", + Self::Action => "Add action", + Self::Observation => "Observe state", + Self::Fragment => "Add fragment", + } + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum MessageStyle { + Asynchronous, + Call, + CallReply, + Reply, + Create, + Delete, + Timer, +} +impl std::fmt::Display for MessageStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Asynchronous => "Asynchronous message", + Self::Call => "Synchronous call", + Self::CallReply => "Call + correlated reply", + Self::Reply => "Reply to existing call", + Self::Create => "Create participant", + Self::Delete => "Delete participant", + Self::Timer => "Local timer", + }) + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Context { + pub document: Id, + pub scenario: Id, + pub revision: u64, + pub epoch: u64, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Slot { + pub parent: Id, + pub before: Option, +} +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum CanvasTool { + #[default] + Select, + Message(MessageStyle), + Add(Tool), +} +impl std::fmt::Display for CanvasTool { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Select => f.write_str("Select / reorder"), + Self::Message(style) => write!(f, "{style}"), + Self::Add(tool) => f.write_str(tool.label()), + } + } +} +#[derive(Debug, Clone)] +pub enum CanvasEvent { + First(Context, Tool), + Scrolled(Context, ScrollArea, iced::Vector), + Restored(Context, Id, ScrollArea, iced::Vector), + Viewport(Context, Size), + FormShown(Context, Id), + Zoom(Context, Option), + Camera(Context, u64, super::canvas_workspace::Event), + Focus(Context), + Shelf(Context, Shelf), + OpenUse(Context, Id), + Back(Context), + Choose(Context, CanvasTool), + Select(Context, Id), + Begin { + context: Context, + slot: Slot, + sender: Id, + receiver: Id, + style: MessageStyle, + }, + BeginOccurrence { + context: Context, + tool: Tool, + slot: Slot, + lifeline: Option, + }, + Reorder { + context: Context, + occurrence: Id, + slot: Slot, + }, +} +#[derive(Debug, Clone, Copy)] +pub enum ScrollArea { + Canvas, + // Retained navigation channel, now the independent Selection/outline shelf. + Page, +} +impl CanvasEvent { + pub fn context(&self) -> Context { + match self { + Self::First(context, _) + | Self::Scrolled(context, _, _) + | Self::Restored(context, ..) + | Self::Viewport(context, _) + | Self::FormShown(context, _) + | Self::Zoom(context, _) + | Self::Camera(context, ..) + | Self::Focus(context) + | Self::Shelf(context, _) + | Self::OpenUse(context, _) + | Self::Back(context) + | Self::Choose(context, _) + | Self::Select(context, _) + | Self::Begin { context, .. } + | Self::BeginOccurrence { context, .. } + | Self::Reorder { context, .. } => *context, + } + } +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Operator(pub FragmentOperator); +impl std::fmt::Display for Operator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self.0 { + FragmentOperator::Alternative => "ALT · alternatives", + FragmentOperator::Parallel => "PAR · parallel", + FragmentOperator::Loop => "LOOP · repetition", + FragmentOperator::Optional => "OPT · optional", + }) + } +} +#[derive(Debug, Clone)] +pub enum Event { + Source(super::source_draft::Event), + Start(Id, Tool), + Cancel, + Submit, + Name(String), + Target(Choice), + Exchange(Choice), + Sender(Choice), + Receiver(Choice), + Call(Choice), + Style(MessageStyle), + OperationIntent(bool), + Operation(Choice), + ClearOperation, + Parent(Choice), + Expression(String), + SecondGuard(String), + Operator(Operator), + Binding(Id, Choice), + Value(crate::data_values::FieldPathKey, String), + ValueFocus(crate::data_values::FieldPathKey), + ValueBrowse(Option), + ValuePage(ValueSection, usize), + ChoiceSearch(String), + ChoicePage(usize), + AppendInstead, +} +#[derive(Debug, Clone, Copy)] +pub enum ValueSection { + Fields, + Authored, +} +const VALUE_PAGE_SIZE: usize = 24; +#[derive(Default)] +pub struct State { + draft: Option, + form_session: Id, + form_mount: Id, + form_focus_pending: bool, + canvas_tool: Option<(Id, CanvasTool)>, + call_choices: RefCell)>>, + scroll_offsets: [iced::Vector; 2], + return_to: Vec, + restore: Option, + presentation: workspace::Presentation, +} +const CALLER_LIMIT: usize = 32; +pub(crate) struct Caller { + document: Id, + pub owner: Id, + pub occurrence: Id, + target: Id, + tool: CanvasTool, + offsets: [iced::Vector; 2], + presentation: workspace::Presentation, +} +struct ViewportRestore { + document: Id, + owner: Id, + token: Id, + offsets: [iced::Vector; 2], + pending: [bool; 2], +} +#[derive(Clone, Copy)] +pub(super) struct ScrollRestore { + pub document: Id, + pub owner: Id, + pub token: Id, + pub offset: iced::Vector, +} +#[derive(PartialEq, Eq)] +struct CallChoiceContext { + document: Id, + revision: u64, + owner: Id, + parent: Id, + before: Option, + endpoints: Option<(Option, Option)>, +} +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct Draft { + #[serde(default)] + source: super::source_draft::State, + #[serde(default = "Id::new_v4")] + invocation: Id, + #[serde(default)] + source_origin: Option<(Id, oa_core::Element)>, + + owner: Id, + parent: Id, + before: Option, + fixed_endpoints: bool, + tool: Tool, + name: String, + target: Option, + exchange: Option, + sender: Option, + receiver: Option, + call: Option, + style: MessageStyle, + #[serde(default)] + operation: Option, + #[serde(default)] + disabled_operation: Option, + expression: String, + second_guard: String, + operator: FragmentOperator, + bindings: BTreeMap, + #[serde(with = "recovery_values")] + values: BTreeMap, + #[serde(with = "recovery_field_key")] + value_focus: Option, + #[serde(with = "recovery_field_key")] + value_browse: Option, + value_browse_page: usize, + value_authored_page: usize, + choice_search: String, + choice_page: usize, +} +impl State { + pub(crate) fn camera_boundary(&self) -> impl PartialEq + use<> { + ( + self.canvas_tool, + self.presentation.focused, + self.presentation.shelf, + self.presentation.viewport, + self.presentation.pan, + self.presentation.zoom, + self.presentation.fit, + ) + } + + pub(super) fn pending_name(&self) -> Option<&str> { + self.draft.as_ref().map(|d| d.name.as_str()) + } + pub(super) fn source_view<'a>(&'a self, p: &'a Project) -> Option> { + self.draft + .as_ref()? + .source + .view(p) + .map(|v| v.map(Event::Source)) + } + + pub fn source_active(&self) -> bool { + self.draft.as_ref().is_some_and(|d| d.source.engaged()) + } + + pub fn source_open(&self) -> bool { + self.draft.as_ref().is_some_and(|d| d.source.is_open()) + } + pub fn source_back(&mut self) -> bool { + let returned = self.draft.as_mut().is_some_and(|d| d.source.back()); + if returned { + self.remount_form(); + } + returned + } + + pub(crate) fn enter(&mut self, document: Id, owner: Id) { + if self.presentation.identity != Some((document, owner)) { + self.presentation = workspace::Presentation { + identity: Some((document, owner)), + focused: self.presentation.focused, + ..Default::default() + }; + self.scroll_offsets = [iced::Vector::ZERO; 2]; + self.restore = None; + } + } + pub(crate) fn viewport(&mut self, size: Size) -> bool { + self.presentation.viewport(size) + } + pub(crate) fn camera(&mut self, event: super::canvas_workspace::Event) -> bool { + let p = &mut self.presentation; + super::canvas_workspace::change(&mut p.pan, &mut p.zoom, &mut p.fit, event) + } + pub(crate) fn zoom(&mut self, scale: Option) -> bool { + self.presentation.zoom(scale) + } + pub(crate) fn camera_zoom(&mut self, p: &Project, owner: Id, zoom: Option) -> bool { + let size = Scene::sequence(p, owner, owner).size; + let old = self.presentation.scale(size); + if let Some(next) = zoom { + let viewport = self.presentation.viewport.unwrap_or(Size::new(500., 300.)); + self.camera(super::canvas_workspace::Event::Zoom { + factor: next / old, + at: Point::new(viewport.width / 2., viewport.height / 2.), + base: old, + }) + } else { + self.zoom(None); + let fitted = self.presentation.scale(size); + self.zoom(Some(fitted)); + self.presentation.pan = iced::Vector::ZERO; + true + } + } + pub(crate) fn focused(&self) -> bool { + self.presentation.focused + } + pub(crate) fn toggle_focus(&mut self) { + self.presentation.focused = !self.presentation.focused; + } + pub(crate) fn toggle_shelf(&mut self, shelf: Shelf) { + self.presentation.shelf = (self.presentation.shelf != Some(shelf)).then_some(shelf); + } + pub(crate) fn open_use( + &mut self, + p: &Project, + context: Context, + occurrence: Id, + ) -> Result { + let target = referenced_scenario(p, context.scenario, occurrence) + .ok_or("Choose a scenario reference in the current scenario.")?; + self.return_to + .retain(|frame| frame.document == context.document); + if self.return_to.len() == CALLER_LIMIT { + self.return_to.remove(0); + } + self.return_to.push(Caller { + document: context.document, + owner: context.scenario, + occurrence, + target, + tool: self.canvas_tool(context.scenario), + offsets: self.scroll_offsets, + presentation: self.presentation.clone(), + }); + self.canvas_tool = Some((target, CanvasTool::Select)); + self.scroll_offsets = [iced::Vector::ZERO; 2]; + self.restore = None; + Ok(target) + } + fn valid_caller(p: &Project, context: Context, frame: &Caller) -> bool { + frame.document == context.document + && frame.target == context.scenario + && referenced_scenario(p, frame.owner, frame.occurrence) == Some(frame.target) + } + pub(super) fn can_back(&self, p: &Project, context: Context) -> bool { + self.return_to + .iter() + .any(|frame| Self::valid_caller(p, context, frame)) + } + pub(crate) fn back(&mut self, p: &Project, context: Context) -> Option { + while let Some(frame) = self.return_to.pop() { + if Self::valid_caller(p, context, &frame) { + return Some(frame); + } + } + None + } + pub(crate) fn restore_caller(&mut self, context: Context, frame: Caller) { + if context.document != frame.document || context.scenario != frame.owner { + return; + } + self.canvas_tool = Some((frame.owner, frame.tool)); + self.scroll_offsets = frame.offsets; + self.presentation = frame.presentation; + self.presentation.identity = Some((context.document, context.scenario)); + self.restore = Some(ViewportRestore { + document: context.document, + owner: frame.owner, + token: Id::new_v4(), + offsets: frame.offsets, + pending: [false, true], + }); + } + pub(super) fn scroll_restore( + &self, + context: Context, + area: ScrollArea, + ) -> Option { + let index = match area { + ScrollArea::Canvas => 0, + ScrollArea::Page => 1, + }; + let restore = self.restore.as_ref().filter(|r| { + r.document == context.document && r.owner == context.scenario && r.pending[index] + })?; + Some(ScrollRestore { + document: restore.document, + owner: restore.owner, + token: restore.token, + offset: restore.offsets[index], + }) + } + pub(crate) fn restored( + &mut self, + context: Context, + token: Id, + area: ScrollArea, + offset: iced::Vector, + ) -> bool { + let index = match area { + ScrollArea::Canvas => 0, + ScrollArea::Page => 1, + }; + let Some(restore) = self.restore.as_mut().filter(|r| { + r.document == context.document + && r.owner == context.scenario + && r.token == token + && r.pending[index] + }) else { + return false; + }; + restore.pending[index] = false; + if !restore.pending.contains(&true) { + self.restore = None; + } + self.scrolled(area, offset) + } + pub(super) fn recovery_snapshot(&self) -> Result, String> { + self.draft + .as_ref() + .map(serde_json::to_value) + .transpose() + .map_err(|e| e.to_string()) + } + + pub(super) fn from_recovery(value: serde_json::Value) -> Result { + let draft = serde_json::from_value(value).map_err(|e| e.to_string())?; + let session = Id::new_v4(); + Ok(Self { + draft: Some(draft), + form_session: session, + form_mount: session, + form_focus_pending: true, + ..Default::default() + }) + } + + pub(crate) fn form_session(&self) -> Option { + self.draft.as_ref().map(|_| self.form_session) + } + + pub(crate) fn form_mount(&self) -> Option { + self.draft + .as_ref() + .filter(|d| !d.source.is_open()) + .map(|_| self.form_mount) + } + + pub(crate) fn focus_stamp(&self, context: Context) -> Option { + (self.draft.as_ref()?.owner == context.scenario).then_some(focus::Stamp::new( + context, + self.form_session, + self.form_mount()?, + )) + } + + fn remount_form(&mut self) { + self.form_mount = Id::new_v4(); + self.form_focus_pending = true; + } + + pub(crate) fn take_form_focus(&mut self, mount: Id) -> bool { + if self.form_mount() == Some(mount) && self.form_focus_pending { + self.form_focus_pending = false; + true + } else { + false + } + } + + pub(crate) fn scrolled(&mut self, area: ScrollArea, offset: iced::Vector) -> bool { + if !offset.x.is_finite() || !offset.y.is_finite() || offset.x < 0. || offset.y < 0. { + return false; + } + let index = match area { + ScrollArea::Canvas => 0, + ScrollArea::Page => 1, + }; + let changed = self.scroll_offsets[index] != offset; + self.scroll_offsets[index] = offset; + changed + } + #[cfg(test)] + fn scroll_offset(&self) -> iced::Vector { + // The former page scroll now belongs to a sibling outline shelf. + self.scroll_offsets[0] + } + fn call_choices(&self, p: &Project, context: Context, draft: &Draft) -> Vec { + let key = CallChoiceContext { + document: context.document, + revision: context.revision, + owner: draft.owner, + parent: draft.parent, + before: draft.before, + endpoints: draft + .fixed_endpoints + .then_some((draft.sender, draft.receiver)), + }; + let mut cached = self.call_choices.borrow_mut(); + if cached.as_ref().is_none_or(|(previous, _)| *previous != key) { + *cached = Some((key, calls(p, draft))); + } + cached.as_ref().unwrap().1.clone() + } + pub(crate) fn created_selection(&self, p: &Project, command: &Command) -> Option { + let draft = self.draft.as_ref()?; + if self + .draft + .as_ref() + .is_some_and(|d| d.source.label(d.target).is_some()) + && self.prepare_pending(p).ok().as_ref() != Some(command) + { + return None; + } + fn created(command: &Command, parent: Id) -> Option { + match command { + Command::CreateElement(element) if element.parent == Some(parent) => { + Some(element.id) + } + Command::Batch(commands) => { + commands.iter().find_map(|command| created(command, parent)) + } + _ => None, + } + } + created(command, draft.parent) + } + pub(super) fn canvas_tool(&self, owner: Id) -> CanvasTool { + self.canvas_tool + .filter(|(id, _)| *id == owner) + .map(|(_, tool)| tool) + .unwrap_or_default() + } + pub(crate) fn choose_tool(&mut self, owner: Id, tool: CanvasTool) { + self.canvas_tool = Some((owner, tool)); + } + pub(crate) fn begin_message( + &mut self, + p: &Project, + context: Context, + slot: Slot, + sender: Id, + receiver: Id, + style: MessageStyle, + ) -> Result<(), String> { + slot_index(p, context.scenario, slot)?; + for line in [sender, receiver] { + if p.elements + .get(&line) + .is_none_or(|e| e.kind() != Kind::Lifeline || e.parent != Some(context.scenario)) + || represented(p, line).is_none() + { + return Err("Choose represented lifelines in this scenario.".into()); + } + } + self.update(p, Event::Start(context.scenario, Tool::Message))?; + let draft = self.draft.as_mut().expect("Start installed the draft"); + draft.parent = slot.parent; + draft.before = slot.before; + draft.fixed_endpoints = true; + draft.sender = Some(sender); + draft.receiver = (style != MessageStyle::Timer).then_some(receiver); + draft.style = style; + Ok(()) + } + pub(crate) fn begin_occurrence( + &mut self, + p: &Project, + context: Context, + tool: Tool, + slot: Slot, + lifeline: Option, + ) -> Result<(), String> { + if !tool.canvas_occurrence() { + return Err( + "Choose an action, state observation, scenario reference or fragment.".into(), + ); + } + slot_index(p, context.scenario, slot)?; + if let Some(line) = lifeline + && (!tool.uses_lifeline() + || p.elements.get(&line).is_none_or(|e| { + e.kind() != Kind::Lifeline || e.parent != Some(context.scenario) + }) + || represented(p, line).is_none()) + { + return Err("Choose a represented lifeline in this scenario.".into()); + } + self.update(p, Event::Start(context.scenario, tool))?; + let draft = self.draft.as_mut().expect("Start installed the draft"); + draft.parent = slot.parent; + draft.before = slot.before; + draft.sender = lifeline; + Ok(()) + } + pub(super) fn pending_placement(&self, owner: Id) -> Option<(Slot, Option)> { + let draft = self + .draft + .as_ref() + .filter(|d| d.owner == owner && d.tool != Tool::Lifeline)?; + Some(( + Slot { + parent: draft.parent, + before: draft.before, + }, + draft.sender, + )) + } + pub(super) fn pending_owner(&self) -> Option { + self.draft.as_ref().map(|d| d.owner) + } + + pub(super) fn prepare_pending(&self, p: &Project) -> Result { + let command = self + .draft + .as_ref() + .ok_or("Choose a scenario tool first.")? + .command(p)?; + execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?; + Ok(command) + } + + pub(super) fn acknowledge_commit(&mut self) { + self.draft = None; + *self.call_choices.get_mut() = None; + } + + pub fn update(&mut self, p: &Project, event: Event) -> Result, String> { + if let Event::Source(event) = event { + if self + .draft + .as_mut() + .ok_or("Start the consumer form first.")? + .source + .dismiss(&event)? + { + self.remount_form(); + return Ok(None); + } + let d = self + .draft + .as_ref() + .ok_or("Start the consumer form first.")?; + if d.tool != Tool::Lifeline { + return Err("This reference does not support staged source creation.".into()); + } + if d.source_origin.as_ref().is_some_and(|(document, owner)| { + *document != p.id || p.elements.get(&d.owner) != Some(owner) + }) { + return Err("The original consumer changed. Cancel and reopen its form; the current draft is retained.".into()); + } + if d.source_origin.is_none() && d.source.engaged() { + return Err("The recovered source draft has no original document binding. Go Back or cancel it.".into()); + } + let mut next = d.clone(); + if next.source_origin.is_none() && matches!(&event, super::source_draft::Event::Open(_)) + { + next.source_origin = Some(( + p.id, + p.elements + .get(&d.owner) + .ok_or("The consumer owner no longer exists.")? + .clone(), + )); + } + if let Some(id) = next.source.update( + p, + super::source_draft::Scope::Lifeline { + owner: d.owner, + viewpoint: viewpoint(p, d.owner), + }, + d.invocation, + event, + )? { + next.target = Some(id); + next.command(p)?; + } + let returned = d.source.is_open() && !next.source.is_open(); + self.draft = Some(next); + if returned { + self.remount_form(); + } + return Ok(None); + } + if self.source_open() && !matches!(&event, Event::Cancel) { + return Err("Use the new source or go Back before editing the parent form.".into()); + } + + if self.draft.is_some() && matches!(&event, Event::Start(..)) { + return Err(PENDING_DRAFT.into()); + } + match event { + Event::Start(owner, tool) => { + if p.elements.get(&owner).map(oa_core::Element::kind) != Some(Kind::Scenario) { + return Err("Choose a scenario.".into()); + } + self.form_session = Id::new_v4(); + self.form_mount = self.form_session; + self.form_focus_pending = true; + self.draft = Some(Draft { + source: Default::default(), + invocation: Id::new_v4(), + source_origin: Some((p.id, p.elements[&owner].clone())), + owner, + parent: owner, + before: None, + fixed_endpoints: false, + tool, + name: String::new(), + target: None, + exchange: None, + sender: None, + receiver: None, + call: None, + style: MessageStyle::CallReply, + operation: None, + disabled_operation: None, + expression: String::new(), + second_guard: "else".into(), + operator: FragmentOperator::Alternative, + bindings: BTreeMap::new(), + values: BTreeMap::new(), + value_focus: None, + value_browse: None, + value_browse_page: 0, + value_authored_page: 0, + choice_search: String::new(), + choice_page: 0, + }); + } + Event::Cancel => self.acknowledge_commit(), + Event::Submit => return self.prepare_pending(p).map(Some), + other => { + if let Some(d) = &mut self.draft { + match other { + Event::Name(s) => d.name = s, + Event::Expression(s) => d.expression = s, + Event::SecondGuard(s) => d.second_guard = s, + Event::OperationIntent(enabled) => { + if enabled { + d.operation = Some( + d.operation + .clone() + .or_else(|| d.disabled_operation.take()) + .unwrap_or(OperationUse { allocation: None }), + ); + } else { + d.disabled_operation = + d.operation.take().or_else(|| d.disabled_operation.take()); + } + } + Event::Operation(c) => { + d.operation = Some(OperationUse { + allocation: Some(c.id), + }) + } + Event::ClearOperation => { + d.operation = Some(OperationUse { allocation: None }) + } + Event::Target(c) => { + d.target = Some(c.id); + d.bindings.clear(); + } + Event::Exchange(c) => { + if d.exchange != Some(c.id) { + d.exchange = Some(c.id); + if !d.has_operation_intent(p) { + d.reset_values(); + } + if !d.fixed_endpoints { + d.sender = None; + d.receiver = None; + } + } + } + Event::Sender(c) => d.sender = Some(c.id), + Event::Receiver(c) => d.receiver = Some(c.id), + Event::Parent(c) => { + if d.parent != c.id { + d.parent = c.id; + d.before = None; + } + } + Event::ChoiceSearch(value) => { + d.choice_search = value; + d.choice_page = 0; + } + Event::ChoicePage(page) => d.choice_page = page, + Event::AppendInstead => d.before = None, + Event::Style(style) => { + let previous = d.style; + d.style = style; + if previous != style { + if previous == MessageStyle::Timer || style == MessageStyle::Timer { + d.exchange = None; + d.sender = None; + d.receiver = None; + d.call = None; + } else if (previous == MessageStyle::Reply) + != (style == MessageStyle::Reply) + { + std::mem::swap(&mut d.sender, &mut d.receiver); + d.call = None; + } + if !d.has_operation_intent(p) { + d.retain_value_paths(p); + } + } + } + Event::Call(c) => { + d.call = Some(c.id); + if let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + exchange, + sender, + receiver, + .. + }, + .. + }) = p.elements.get(&c.id).and_then(|e| e.shared()) + { + if d.exchange != *exchange && !d.has_operation_intent(p) { + d.reset_values(); + } + d.exchange = *exchange; + d.sender = *receiver; + d.receiver = *sender; + } + } + Event::Operator(operator) => d.operator = operator.0, + Event::Binding(referenced, c) => { + d.bindings.insert(referenced, c.id); + } + Event::Value(id, s) => { + d.values.insert(id, s); + } + Event::ValueFocus(key) + if d.signature(p) + .ok() + .flatten() + .and_then(|r| { + crate::data_values::resolve(p, &r, &d.value_kind(), &key) + }) + .is_some() => + { + d.value_focus = Some(key); + } + Event::ValueBrowse(key) => { + if key.as_ref().is_none_or(|key| { + d.signature(p) + .ok() + .flatten() + .and_then(|r| { + crate::data_values::resolve(p, &r, &d.value_kind(), key) + }) + .is_some() + }) { + d.value_browse = key; + d.value_browse_page = 0; + } + } + Event::ValuePage(section, page) => match section { + ValueSection::Fields => d.value_browse_page = page, + ValueSection::Authored => d.value_authored_page = page, + }, + _ => {} + } + } + } + } + Ok(None) + } +} + +mod recovery_values { + use super::*; + use serde::{Deserialize, Serialize}; + pub fn serialize( + values: &BTreeMap, + serializer: S, + ) -> Result { + values + .iter() + .map(|(key, value)| (&key.path, key.field, value)) + .collect::>() + .serialize(serializer) + } + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let values = Vec::<(Vec, Id, String)>::deserialize(deserializer)?; + let mut result = BTreeMap::new(); + for (path, field, value) in values { + if result + .insert(crate::data_values::FieldPathKey { path, field }, value) + .is_some() + { + return Err(serde::de::Error::custom( + "Duplicate recovered scenario value path", + )); + } + } + Ok(result) + } +} + +mod recovery_field_key { + use super::*; + use serde::{Deserialize, Serialize}; + pub fn serialize( + key: &Option, + serializer: S, + ) -> Result { + key.as_ref() + .map(|key| (&key.path, key.field)) + .serialize(serializer) + } + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + Ok(Option::<(Vec, Id)>::deserialize(deserializer)? + .map(|(path, field)| crate::data_values::FieldPathKey { path, field })) + } +} +fn viewpoint(p: &Project, owner: Id) -> ScenarioViewpoint { + match p.elements.get(&owner).and_then(|e| e.shared()) { + Some(SharedBody::Scenario { viewpoint, .. }) => *viewpoint, + _ => ScenarioViewpoint::MixedLegacy, + } +} +pub(crate) fn referenced_scenario(p: &Project, owner: Id, occurrence: Id) -> Option { + if p.elements.get(&owner)?.kind() != Kind::Scenario + || validation::ancestor_of_kind(p, occurrence, Kind::Scenario) != Some(owner) + { + return None; + } + let SharedBody::Occurrence { + body: + OccurrenceBody::ScenarioUse { + scenario: Some(target), + .. + }, + .. + } = p.elements.get(&occurrence)?.shared()? + else { + return None; + }; + p.elements + .get(target) + .filter(|e| e.kind() == Kind::Scenario) + .map(|e| e.id) +} +fn represented(p: &Project, line: Id) -> Option { + match p.elements.get(&line)?.shared()? { + SharedBody::Lifeline { represented } => *represented, + _ => None, + } +} +fn named_choice(p: &Project, id: Id) -> Choice { + Choice { + id, + label: format!( + "{} · {}", + p.named(id), + p.elements + .get(&id) + .map(|e| e.alias.as_str()) + .or_else(|| p.relations.get(&id).map(|r| r.alias.as_str())) + .unwrap_or("") + ), + } +} +fn lines(p: &Project, owner: Id) -> Vec { + p.children(owner) + .filter(|e| e.kind() == Kind::Lifeline) + .map(|e| named_choice(p, e.id)) + .collect() +} +fn parents(p: &Project, owner: Id) -> Vec { + let mut out = vec![Choice { + id: owner, + label: "Main scenario sequence".into(), + }]; + out.extend( + p.elements + .values() + .filter(|e| { + matches!( + e.shared(), + Some(SharedBody::Occurrence { + body: OccurrenceBody::Branch { .. }, + .. + }) + ) && validation::ancestor_of_kind(p, e.id, Kind::Scenario) == Some(owner) + }) + .map(|e| named_choice(p, e.id)), + ); + out +} +fn slot_index(p: &Project, owner: Id, slot: Slot) -> Result { + if !parents(p, owner) + .iter() + .any(|choice| choice.id == slot.parent) + { + return Err("Choose an insertion slot in this scenario or one of its operands.".into()); + } + let steps = p + .elements + .get(&slot.parent) + .and_then(oa_core::Element::steps) + .ok_or("This sequence no longer exists.")?; + slot.before.map_or(Ok(steps.len()), |id| { + steps + .iter() + .position(|step| *step == id) + .ok_or_else(|| "The insertion slot changed. Choose its position again.".into()) + }) +} +pub(super) fn slot_label(p: &Project, slot: Slot) -> String { + match slot.before { + Some(id) => format!( + "{} · before {}", + named_choice(p, slot.parent).label, + named_choice(p, id).label + ), + None => format!("{} · append", named_choice(p, slot.parent).label), + } +} +pub(crate) fn reorder( + p: &Project, + owner: Id, + occurrence: Id, + slot: Slot, +) -> Result { + let to = slot_index(p, owner, slot)?; + if p.elements.get(&occurrence).and_then(|e| e.parent) != Some(slot.parent) { + return Err( + "Reorder inside the occurrence's current sequence; dragging never changes its operand." + .into(), + ); + } + let mut steps = p.elements[&slot.parent] + .steps() + .expect("slot validated") + .clone(); + let from = steps + .iter() + .position(|id| *id == occurrence) + .ok_or("This occurrence is not in the sequence.")?; + steps.remove(from); + steps.insert(to - usize::from(from < to), occurrence); + let command = Command::SetSteps(slot.parent, steps); + execute_batch(p, std::slice::from_ref(&command)).map_err(|error| error.to_string())?; + Ok(command) +} +fn compatible_binding(p: &Project, referenced: Id, local: Id) -> bool { + represented(p, referenced) + .zip(represented(p, local)) + .is_some_and(|(a, b)| validation::semantic_representation_compatible(p, b, a) == Some(true)) +} +fn valid_body_choice(p: &Project, owner: Id, body: OccurrenceBody) -> bool { + let e = occurrence(p, owner, "Scenario selection preview", body); + execute_batch(p, &[Command::CreateElement(e)]).is_ok() +} +fn sources(p: &Project, d: &Draft) -> Vec { + let Some(owner) = p + .elements + .get(&d.owner) + .filter(|e| e.kind() == Kind::Scenario) + else { + return vec![]; + }; + let layer = owner.layer(); + let mut values: Vec<_> = p + .elements + .values() + .filter(|e| { + e.layer() == layer + && match d.tool { + Tool::Lifeline => { + validation::viewpoint_represents(viewpoint(p, d.owner), e.kind()) + } + Tool::Action => e.kind().is_work(), + Tool::Observation => e.kind().is_state(), + Tool::Reuse => { + e.kind() == Kind::Scenario + && valid_body_choice( + p, + d.owner, + OccurrenceBody::ScenarioUse { + scenario: Some(e.id), + bindings: vec![], + }, + ) + } + _ => false, + } + }) + .map(|e| named_choice(p, e.id)) + .collect(); + values.sort_by(|a, b| a.label.cmp(&b.label)); + values +} +fn exchanges(p: &Project, d: &Draft) -> Vec { + // Candidate browsing concerns the declared viewpoint and represented + // endpoints. The complete transaction still validates the chosen slot, + // lifecycle, payload and paired reply on Apply. + let viewpoint = viewpoint(p, d.owner); + p.relations + .values() + .filter(|r| { + p.layer_of(r.id) == p.layer_of(d.owner) + && matches!( + r.kind(), + RelationKind::Interaction + | RelationKind::CommunicationMean + | RelationKind::FunctionalExchange + | RelationKind::BoundaryExchange + ) + && match viewpoint { + ScenarioViewpoint::OperationalActivity => r.kind() == RelationKind::Interaction, + ScenarioViewpoint::Functional => r.kind() == RelationKind::FunctionalExchange, + ScenarioViewpoint::ParticipantInteraction => matches!( + r.kind(), + RelationKind::Interaction + | RelationKind::CommunicationMean + | RelationKind::BoundaryExchange + ), + ScenarioViewpoint::BoundaryInterface => matches!( + r.kind(), + RelationKind::FunctionalExchange | RelationKind::BoundaryExchange + ), + ScenarioViewpoint::MixedLegacy => true, + } + && (!d.fixed_endpoints + || d.sender.zip(d.receiver).is_some_and(|(sender, receiver)| { + let mut body = d.message_body(); + if let OccurrenceBody::Exchange { exchange, .. } = &mut body { + *exchange = Some(r.id); + } + validation::message_endpoints(p, &body).is_some_and(|(source, target)| { + validation::lifeline_matches_endpoint(p, sender, source) == Some(true) + && validation::lifeline_matches_endpoint(p, receiver, target) + == Some(true) + }) + })) + }) + .map(|r| Choice { + id: r.id, + label: format!( + "{} · {} {} {}", + r.name, + p.named(r.source()), + if matches!( + r.body, + RelationBody::CommunicationMean { + oriented: false, + .. + } | RelationBody::BoundaryExchange { + oriented: false, + .. + } + ) { + "↔" + } else { + "→" + }, + p.named(r.target()) + ), + }) + .collect() +} +fn calls(p: &Project, d: &Draft) -> Vec { + p.elements + .values() + .filter(|e| validation::ancestor_of_kind(p, e.id, Kind::Scenario) == Some(d.owner)) + .filter_map(|e| { + let Some(SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + message_kind: MessageKind::Call, + exchange, + sender, + receiver, + .. + }, + .. + }) = e.shared() + else { + return None; + }; + if d.fixed_endpoints && (d.sender != *receiver || d.receiver != *sender) { + return None; + } + let mut candidate = d.clone(); + candidate.style = MessageStyle::Reply; + candidate.call = Some(e.id); + // Candidate availability must not fluctuate while typing names or + // expressions. These draft values are validated only on Apply. + candidate.reset_values(); + candidate.name.clear(); + candidate.exchange = *exchange; + candidate.sender = *receiver; + candidate.receiver = *sender; + candidate + .command(p) + .ok() + .and_then(|command| execute_batch(p, &[command]).ok()) + .map(|_| named_choice(p, e.id)) + }) + .collect() +} +impl Draft { + fn has_operation_intent(&self, p: &Project) -> bool { + self.operation.is_some() + || self + .call + .and_then(|id| p.elements.get(&id)) + .is_some_and(|e| { + matches!( + e.shared(), + Some(SharedBody::Occurrence { + body: OccurrenceBody::Exchange { + operation: Some(_), + .. + }, + .. + }) + ) + }) + } + fn proposed_message(&self, p: &Project) -> oa_core::Element { + let mut message = self.append(p, "Operation preview", self.message_body()); + // This is a transient resolver input; no generated identity reaches a command. + message.id = Id::nil(); + message + } + fn signature(&self, p: &Project) -> Result, String> { + crate::data_values::message_signature(p, &self.proposed_message(p)) + } + fn reset_values(&mut self) { + self.values.clear(); + self.value_focus = None; + self.value_browse = None; + self.value_browse_page = 0; + self.value_authored_page = 0; + } + fn retain_value_paths(&mut self, p: &Project) { + let kind = self.kind(); + let exchange = self.exchange.and_then(|id| p.relations.get(&id)); + self.values.retain(|key, _| { + exchange + .and_then(|r| crate::data_values::resolve(p, r, &kind, key)) + .is_some() + }); + if self.value_focus.as_ref().is_some_and(|key| { + exchange + .and_then(|r| crate::data_values::resolve(p, r, &kind, key)) + .is_none() + }) { + self.value_focus = None; + } + if self.value_browse.as_ref().is_some_and(|key| { + exchange + .and_then(|r| crate::data_values::resolve(p, r, &kind, key)) + .is_none() + }) { + self.value_browse = None; + self.value_browse_page = 0; + } + } + fn browsed_values( + &self, + p: &Project, + exchange: &Relation, + ) -> Result, String> { + match &self.value_browse { + Some(key) => crate::data_values::children(p, exchange, &self.value_kind(), key), + None => Ok(crate::data_values::roots(p, exchange, &self.value_kind())), + } + } + fn authored_values(&self) -> Vec<(&crate::data_values::FieldPathKey, &str)> { + self.values + .iter() + .filter(|(_, value)| !value.trim().is_empty()) + .map(|(key, value)| (key, value.as_str())) + .collect() + } + fn value_kind(&self) -> MessageKind { + crate::data_values::operation_value_phase(&self.kind(), self.operation.as_ref()) + } + fn kind(&self) -> MessageKind { + match self.style { + MessageStyle::Asynchronous => MessageKind::Asynchronous, + MessageStyle::Call | MessageStyle::CallReply => MessageKind::Call, + MessageStyle::Reply => MessageKind::Reply, + MessageStyle::Create => MessageKind::Create, + MessageStyle::Delete => MessageKind::Delete, + MessageStyle::Timer => MessageKind::Timer { + expression: self.expression.clone(), + }, + } + } + fn message_body(&self) -> OccurrenceBody { + OccurrenceBody::Exchange { + exchange: if self.style == MessageStyle::Timer { + None + } else { + self.exchange + }, + sender: self.sender, + receiver: if self.style == MessageStyle::Timer { + None + } else { + self.receiver + }, + message_kind: self.kind(), + operation: if matches!( + self.style, + MessageStyle::Call | MessageStyle::CallReply | MessageStyle::Asynchronous + ) { + self.operation.clone() + } else { + None + }, + reply_to: self.call, + values: self + .values + .iter() + .filter(|(_, v)| !v.trim().is_empty()) + .map(|(field, expression)| MessageValue { + path: field.path.clone(), + field: field.field, + expression: expression.clone(), + }) + .collect(), + } + } + fn line_choices(&self, p: &Project, sender: bool) -> Vec { + let endpoint = validation::message_endpoints(p, &self.message_body()) + .map(|(a, b)| if sender { a } else { b }); + lines(p, self.owner) + .into_iter() + .filter(|line| { + if self + .exchange + .and_then(|id| p.relations.get(&id)) + .is_some_and(|r| { + matches!( + r.body, + RelationBody::CommunicationMean { .. } + | RelationBody::BoundaryExchange { + oriented: false, + .. + } + ) + }) + { + let mut body = self.message_body(); + if let OccurrenceBody::Exchange { + sender: from, + receiver: to, + .. + } = &mut body + { + if sender { + *from = Some(line.id); + } else { + *to = Some(line.id); + } + } + return !matches!( + validation::resolve_message_endpoints(p, &body), + validation::MessageEndpointResolution::Conflict + ); + } + endpoint.is_none_or(|endpoint| { + validation::lifeline_matches_endpoint(p, line.id, endpoint) == Some(true) + }) + }) + .collect() + } + fn append(&self, p: &Project, name: &str, body: OccurrenceBody) -> oa_core::Element { + let mut e = occurrence(p, self.owner, name, body); + e.parent = Some(self.parent); + e + } + fn command(&self, p: &Project) -> Result { + if self.source.engaged() + && self.source_origin.as_ref().is_none_or(|(document, owner)| { + *document != p.id || p.elements.get(&self.owner) != Some(owner) + }) + { + return Err("The original consumer document or owner changed. Cancel and reopen its form; your draft is retained.".into()); + } + self.source.check_scope( + super::source_draft::Scope::Lifeline { + owner: self.owner, + viewpoint: viewpoint(p, self.owner), + }, + self.target, + )?; + let preview = self.source.preview(p, self.target)?; + let command = self.consumer_command(preview.as_ref().unwrap_or(p))?; + self.source.combine(p, self.target, command) + } + fn consumer_command(&self, p: &Project) -> Result { + if p.elements.get(&self.owner).map(oa_core::Element::kind) != Some(Kind::Scenario) { + return Err("The scenario no longer exists. Cancel or restore its owner.".into()); + } + if !parents(p, self.owner).iter().any(|c| c.id == self.parent) { + return Err("Choose this scenario or one of its operands as destination.".into()); + } + let default = self + .target + .or(self.exchange) + .map(|id| p.named(id)) + .unwrap_or(self.tool.label()); + let name = if self.name.trim().is_empty() { + default + } else if self.source.label(self.target).is_some() { + self.name.as_str() + } else { + self.name.trim() + }; + let mut commands = vec![]; + match self.tool { + Tool::Lifeline => { + let target = self + .target + .ok_or("Choose the work or participant represented by this lifeline.")?; + if !sources(p, self).iter().any(|c| c.id == target) { + return Err( + "Choose a representation compatible with the scenario viewpoint.".into(), + ); + } + let mut e = p + .make_element( + Kind::Lifeline, + p.elements[&self.owner].layer(), + name, + Some(self.owner), + ) + .unwrap(); + *e.shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(target), + }; + commands.push(Command::CreateElement(e)); + } + Tool::Message => { + let sender = self.sender.ok_or("Choose the sending lifeline.")?; + if !self.line_choices(p, true).iter().any(|c| c.id == sender) { + return Err("The sender must represent the exchange source.".into()); + } + if self.style == MessageStyle::Timer { + if self.expression.trim().is_empty() { + return Err("Enter the timer duration or deadline.".into()); + } + } else { + if self.exchange.is_none() { + return Err("Choose the exchange carried by this message.".into()); + } + let receiver = self.receiver.ok_or("Choose the receiving lifeline.")?; + if !self.line_choices(p, false).iter().any(|c| c.id == receiver) { + return Err("The receiver must represent the exchange target.".into()); + } + } + if self.style == MessageStyle::Reply && self.call.is_none() { + return Err("Choose the earlier call answered by this reply.".into()); + } + let call = self.append(p, name, self.message_body()); + let call_id = call.id; + commands.push(Command::CreateElement(call)); + if self.style == MessageStyle::CallReply { + commands.push(Command::CreateElement(self.append( + p, + &format!("Reply · {name}"), + OccurrenceBody::Exchange { + exchange: self.exchange, + sender: self.receiver, + receiver: self.sender, + message_kind: MessageKind::Reply, + operation: None, + reply_to: Some(call_id), + values: vec![], + }, + ))); + } + } + Tool::Reuse => { + let target = self.target.ok_or("Choose a scenario to reuse.")?; + if !sources(p, self).iter().any(|c| c.id == target) { + return Err("Choose a compatible scenario without recursive reuse.".into()); + } + let mut used = BTreeSet::new(); + let mut bindings = vec![]; + for line in lines(p, target) { + let local = *self + .bindings + .get(&line.id) + .ok_or("Bind every referenced lifeline to a local lifeline.")?; + if !lines(p, self.owner).iter().any(|c| c.id == local) + || !compatible_binding(p, line.id, local) + || !used.insert(local) + { + return Err( + "Each binding needs a distinct compatible local lifeline.".into() + ); + } + bindings.push(LifelineBinding { + referenced: line.id, + local, + }); + } + commands.push(Command::CreateElement(self.append( + p, + name, + OccurrenceBody::ScenarioUse { + scenario: Some(target), + bindings, + }, + ))); + } + Tool::Action | Tool::Observation => { + let target = self + .target + .ok_or("Choose the action or state to observe.")?; + if !sources(p, self).iter().any(|c| c.id == target) { + return Err("Choose compatible work or a state in this layer.".into()); + } + let line = self.sender.ok_or("Choose the observed lifeline.")?; + if !lines(p, self.owner).iter().any(|c| c.id == line) { + return Err("Choose a lifeline in this scenario.".into()); + } + let body = if self.tool == Tool::Action { + OccurrenceBody::Action { + action: Some(target), + lifeline: Some(line), + } + } else { + OccurrenceBody::State { + state: Some(target), + lifeline: Some(line), + } + }; + commands.push(Command::CreateElement(self.append(p, name, body))); + } + Tool::Fragment => { + let two = matches!( + self.operator, + FragmentOperator::Alternative | FragmentOperator::Parallel + ); + if self.operator != FragmentOperator::Parallel && self.expression.trim().is_empty() + { + return Err("Describe the operand condition or loop guard.".into()); + } + let fragment = self.append( + p, + name, + OccurrenceBody::Fragment { + operator: self.operator, + branches: vec![], + }, + ); + let id = fragment.id; + commands.push(Command::CreateElement(fragment)); + for (i, guard) in [&self.expression, &self.second_guard] + .into_iter() + .take(if two { 2 } else { 1 }) + .enumerate() + { + let mut branch = self.append( + p, + &format!("{} · operand {}", name, i + 1), + OccurrenceBody::Branch { + guard: if self.operator == FragmentOperator::Parallel { + String::new() + } else { + guard.clone() + }, + occurrences: vec![], + }, + ); + branch.parent = Some(id); + commands.push(Command::CreateElement(branch)); + } + } + } + if self.tool != Tool::Lifeline { + let slot = Slot { + parent: self.parent, + before: self.before, + }; + let index = slot_index(p, self.owner, slot)?; + let added: Vec<_> = commands + .iter() + .filter_map(|command| match command { + Command::CreateElement(e) + if e.parent == Some(self.parent) && e.kind() == Kind::Occurrence => + { + Some(e.id) + } + _ => None, + }) + .collect(); + let mut steps = p.elements[&self.parent] + .steps() + .expect("slot validated") + .clone(); + steps.splice(index..index, added); + commands.push(Command::SetSteps(self.parent, steps)); + } + Ok(unique_batch(p, commands)) + } +} +fn event(e: Event) -> Message { + Message::Behavior(super::Event::Scenario(e)) +} +fn value_page(len: usize, requested: usize) -> std::ops::Range { + let start = requested.min(len.saturating_sub(1) / VALUE_PAGE_SIZE) * VALUE_PAGE_SIZE; + start..(start + VALUE_PAGE_SIZE).min(len) +} +fn value_pager(len: usize, requested: usize, section: ValueSection) -> Element<'static, Message> { + let range = value_page(len, requested); + let page = range.start / VALUE_PAGE_SIZE; + row![ + button("Previous") + .on_press_maybe((page > 0).then(|| event(Event::ValuePage(section, page - 1)))), + text(format!( + "Page {} of {} · {} fields", + page + 1, + len.div_ceil(VALUE_PAGE_SIZE).max(1), + len + )) + .size(11), + button("Next") + .on_press_maybe((range.end < len).then(|| event(Event::ValuePage(section, page + 1)))) + ] + .spacing(6) + .into() +} +fn value_panel<'a>(p: &Project, d: &'a Draft, exchange: &Relation) -> Element<'a, Message> { + use crate::data_values; + let mut out = column![text("Message values").size(14)].spacing(7); + if let Some(browse) = &d.value_browse { + let title = data_values::resolve(p, exchange, &d.value_kind(), browse) + .map(|choice| format!("Fields inside {}", choice.label)) + .unwrap_or_else(|| "This record path is no longer available in this message.".into()); + out = out.push(text(title).size(11)).push( + row![ + button("Root fields").on_press(event(Event::ValueBrowse(None))), + button("Up one level") + .on_press(event(Event::ValueBrowse(data_values::parent(browse)))) + ] + .spacing(6), + ); + } else { + out = out.push(text("Root fields · choose a record to browse its fields").size(11)); + } + match d.browsed_values(p, exchange) { + Ok(choices) if !choices.is_empty() => { + let range = value_page(choices.len(), d.value_browse_page); + let visible = choices[range].to_vec(); + let selected = visible + .iter() + .find(|choice| Some(&choice.key) == d.value_focus.as_ref()) + .cloned(); + out = out + .push(value_pager( + choices.len(), + d.value_browse_page, + ValueSection::Fields, + )) + .push( + pick_list(visible, selected, |choice| { + event(Event::ValueFocus(choice.key)) + }) + .placeholder("Choose a field on this page…") + .text_size(12) + .padding(8) + .width(Fill), + ); + } + Ok(_) => { + out = out.push(text("No fields at this level for this message phase.").size(11)); + } + Err(error) => out = out.push(text(error).size(11).color(style::MUTED)), + } + match d + .value_focus + .as_ref() + .map(|key| data_values::resolve(p, exchange, &d.value_kind(), key)) + { + Some(Some(focus)) => { + let key = focus.key; + out = out + .push(text(focus.label).size(12)) + .push( + text_input( + "Optional value expression", + d.values.get(&key).map(String::as_str).unwrap_or(""), + ) + .on_input({ + let key = key.clone(); + move |value| event(Event::Value(key.clone(), value)) + }) + .padding(8) + .size(12), + ) + .push(text("An empty expression omits this value.").size(11)); + let mut actions = row![ + button("Show enclosing fields") + .on_press(event(Event::ValueBrowse(data_values::parent(&key)))) + ] + .spacing(6); + if matches!(p.elements.get(&key.field).and_then(oa_core::Element::shared), + Some(SharedBody::DataField { type_ref: TypeRef::Named(record), + storage: FieldStorage::Value, .. }) if data::is_record(p, *record)) + { + actions = actions.push( + button("Browse record fields").on_press(event(Event::ValueBrowse(Some(key)))), + ); + } + out = out.push(actions); + } + Some(None) => { + out = out.push(text("The selected path is unavailable in this message. Choose a compatible field; entered values remain in the draft.") + .size(11).color(style::MUTED)); + } + None => out = out.push(text("Choose a field to enter its value expression.").size(11)), + } + let authored = d.authored_values(); + out = out.push(text(format!("Authored values ({})", authored.len())).size(12)); + if authored.is_empty() { + out = out.push(text("No value expressions entered.").size(11)); + } else { + out = out.push(value_pager( + authored.len(), + d.value_authored_page, + ValueSection::Authored, + )); + for (key, expression) in &authored[value_page(authored.len(), d.value_authored_page)] { + let title = data_values::resolve(p, exchange, &d.value_kind(), key) + .map(|choice| choice.label) + .unwrap_or_else(|| format!("Unavailable field {} · review this value", key.field)); + out = out.push( + button( + column![text(title).size(11), text(short(expression, 100)).size(11)].spacing(3), + ) + .on_press(event(Event::ValueFocus((*key).clone()))) + .width(Fill), + ); + } + } + out.into() +} +fn picker<'a>( + choices: Vec, + selected: Option, + placeholder: &'a str, + change: impl Fn(Choice) -> Event + 'a, +) -> Element<'a, Message> { + let selected = selected + .and_then(|id| choices.iter().find(|c| c.id == id)) + .cloned(); + pick_list(choices, selected, move |c| event(change(c))) + .placeholder(placeholder) + .width(Fill) + .padding(9) + .text_size(12) + .into() +} +fn bounded_picker<'a>( + mut choices: Vec, + d: &'a Draft, + selected: Option, + placeholder: &'a str, + change: impl Fn(Choice) -> Event + 'a, +) -> Element<'a, Message> { + let current = choices + .iter() + .find(|choice| Some(choice.id) == selected) + .cloned(); + let query = d.choice_search.trim().to_lowercase(); + choices.retain(|choice| choice.label.to_lowercase().contains(&query)); + choices.sort_by(|a, b| a.label.cmp(&b.label).then(a.id.cmp(&b.id))); + let len = choices.len(); + let range = value_page(len, d.choice_page); + let page = range.start / VALUE_PAGE_SIZE; + let mut out = column![ + text_input("Search compatible exchanges / calls…", &d.choice_search) + .on_input(|value| event(Event::ChoiceSearch(value))) + .padding(8) + .size(12) + ] + .spacing(6); + if let Some(choice) = current.as_ref().filter(|choice| { + !choices[range.clone()] + .iter() + .any(|visible| visible.id == choice.id) + }) { + out = out.push(label(format!("Selected: {}", choice.label))); + } + if len == 0 { + out = out.push(label("No matching exchange or eligible earlier call. Change the search, endpoints or destination, or cancel.")); + } else { + out = out + .push( + row![ + button("Previous") + .on_press_maybe((page > 0).then(|| event(Event::ChoicePage(page - 1)))), + text(format!( + "{} choices · page {}/{}", + len, + page + 1, + len.div_ceil(VALUE_PAGE_SIZE) + )) + .size(11), + button("Next").on_press_maybe( + (range.end < len).then(|| event(Event::ChoicePage(page + 1))) + ), + ] + .spacing(6), + ) + .push( + pick_list(choices[range].to_vec(), current, move |choice| { + event(change(choice)) + }) + .placeholder(placeholder) + .padding(9) + .text_size(12) + .width(Fill), + ); + } + out.into() +} + +#[cfg(test)] +pub(crate) mod direct_tests { + use super::*; + pub struct Fixture { + pub project: Project, + pub owner: Id, + pub exchange: Id, + pub sender: Id, + pub receiver: Id, + pub call: Id, + pub reply: Id, + } + pub fn fixture() -> Fixture { + let f = crate::data_values::tests::payload(); + let mut p = f.project; + let mut lines = vec![]; + for (name, represented) in [ + ("Caller", p.relations[&f.exchange].source()), + ("Callee", p.relations[&f.exchange].target()), + ] { + let id = p + .add_element(Kind::Lifeline, Layer::Oa, name, Some(f.scenario)) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(represented), + }; + lines.push(id); + } + let mut state = State::default(); + for event in [ + Event::Start(f.scenario, Tool::Message), + Event::Exchange(named_choice(&p, f.exchange)), + Event::Sender(named_choice(&p, lines[0])), + Event::Receiver(named_choice(&p, lines[1])), + ] { + state.update(&p, event).unwrap(); + } + p = execute_batch(&p, &[state.prepare_pending(&p).unwrap()]).unwrap(); + let steps = p.elements[&f.scenario].steps().unwrap(); + let (call, reply) = (steps[0], steps[1]); + Fixture { + project: p, + owner: f.scenario, + exchange: f.exchange, + sender: lines[0], + receiver: lines[1], + call, + reply, + } + } + pub fn context(owner: Id) -> Context { + Context { + document: Id::new_v4(), + scenario: owner, + revision: 0, + epoch: 0, + } + } + pub(super) fn placement_fixture() -> (Fixture, Id, Id, Id, Id, Id) { + let mut f = fixture(); + let p = &mut f.project; + let machine = p + .add_element(Kind::StateMachine, Layer::Oa, "Context", None) + .unwrap(); + let region = p + .add_element(Kind::Region, Layer::Oa, "Main region", Some(machine)) + .unwrap(); + let state = p + .add_element(Kind::State, Layer::Oa, "Ready", Some(region)) + .unwrap(); + let reused = p + .add_element(Kind::Scenario, Layer::Oa, "Reusable check", None) + .unwrap(); + let remote = p + .add_element(Kind::Lifeline, Layer::Oa, "Reused caller", Some(reused)) + .unwrap(); + let represented = represented(p, f.sender); + *p.elements.get_mut(&remote).unwrap().shared_mut().unwrap() = + SharedBody::Lifeline { represented }; + let fragment = occurrence( + p, + f.owner, + "Outer alternative", + OccurrenceBody::Fragment { + operator: FragmentOperator::Alternative, + branches: vec![], + }, + ); + let fragment_id = fragment.id; + *p = execute_batch(p, &[Command::CreateElement(fragment)]).unwrap(); + let branch = occurrence( + p, + fragment_id, + "Chosen operand", + OccurrenceBody::Branch { + guard: "ready".into(), + occurrences: vec![], + }, + ); + let branch_id = branch.id; + *p = execute_batch(p, &[Command::CreateElement(branch)]).unwrap(); + let anchor = occurrence( + p, + branch_id, + "Existing check", + OccurrenceBody::Action { + action: represented, + lifeline: Some(f.sender), + }, + ); + let anchor_id = anchor.id; + *p = execute_batch(p, &[Command::CreateElement(anchor)]).unwrap(); + (f, branch_id, anchor_id, state, reused, remote) + } + fn canvas_mouse( + scene: &Scene, + input: &mut SequenceInput, + event: mouse::Event, + point: Point, + bounds: Rectangle, + ) -> Option { + canvas::Program::update( + scene, + input, + &canvas::Event::Mouse(event), + bounds, + mouse::Cursor::Available(point), + ) + .and_then(|action| action.into_inner().0) + } + fn form(app: &mut App, event: Event) { + let _ = app.update(Message::Behavior(super::super::Event::Scenario(event))); + } + pub fn navigation_fixture() -> (Project, [Id; 3], [Id; 3]) { + let mut p = Project::empty(); + let owners = ["A caller", "B definition", "C definition"].map(|name| { + p.add_element(Kind::Scenario, Layer::Oa, name, None) + .unwrap() + }); + let uses = [ + (owners[0], owners[1]), + (owners[0], owners[1]), + (owners[1], owners[2]), + ] + .map(|(owner, target)| { + let element = occurrence( + &p, + owner, + "Reference", + OccurrenceBody::ScenarioUse { + scenario: Some(target), + bindings: vec![], + }, + ); + let id = element.id; + p = execute_batch(&p, &[Command::CreateElement(element)]).unwrap(); + id + }); + (p, owners, uses) + } + pub fn navigation_event(app: &mut App, event: impl FnOnce(Context) -> CanvasEvent) { + let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(event(context))); + } + #[test] + fn direct_scenario_reference_navigation_retains_each_exact_caller_without_history() { + let (p, [a, b, c], [first, second, nested]) = navigation_fixture(); + let mut app = App::with_project(p.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let source = app.document.bundle().clone(); + let revision = app.document.revision(); + for caller in [second, first] { + navigation_event(&mut app, |ctx| { + CanvasEvent::Choose(ctx, CanvasTool::Add(Tool::Reuse)) + }); + navigation_event(&mut app, |ctx| { + CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(25., 170.)) + }); + navigation_event(&mut app, |ctx| { + CanvasEvent::Scrolled(ctx, ScrollArea::Page, iced::Vector::new(0., 80.)) + }); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, caller)); + assert_eq!(app.selected, b); + assert_eq!(app.behavior.scenario.scroll_offset(), iced::Vector::ZERO); + navigation_event(&mut app, |ctx| { + CanvasEvent::Choose(ctx, CanvasTool::Message(MessageStyle::Timer)) + }); + navigation_event(&mut app, |ctx| { + CanvasEvent::Scrolled(ctx, ScrollArea::Canvas, iced::Vector::new(0., 115.)) + }); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, nested)); + assert_eq!(app.selected, c); + navigation_event(&mut app, CanvasEvent::Back); + assert_eq!(app.selected, nested); + assert_eq!( + app.behavior.scenario.canvas_tool(b), + CanvasTool::Message(MessageStyle::Timer) + ); + assert_eq!( + app.behavior.scenario.scroll_offset(), + iced::Vector::new(0., 115.) + ); + navigation_event(&mut app, CanvasEvent::Back); + assert_eq!(app.selected, caller); + assert_eq!(app.scenario_context().unwrap().scenario, a); + assert_eq!( + app.behavior.scenario.canvas_tool(a), + CanvasTool::Add(Tool::Reuse) + ); + assert_eq!( + app.behavior.scenario.scroll_offset(), + iced::Vector::new(25., 170.) + ); + assert_eq!( + app.behavior.scenario.scroll_offsets[1], + iced::Vector::new(0., 80.), + "The caller's independent outline scroll is restored separately" + ); + assert!( + !app.behavior + .scenario + .can_back(app.project(), app.scenario_context().unwrap()) + ); + assert_eq!(app.project(), &p); + assert_eq!(app.document.bundle(), &source); + assert_eq!(app.document.revision(), revision); + assert!(!app.document.can_undo()); + assert!(!app.document.can_redo()); + } + } + #[test] + fn direct_scenario_reference_navigation_blocks_pending_drafts_and_stale_contexts() { + let (p, [a, b, _], [first, _, _]) = navigation_fixture(); + for back in [false, true] { + for pending in 0..3 { + let mut app = App::with_project(p.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + if back { + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, first)); + } + match pending { + 0 => { + let _ = app.update(Message::Field("/name".into(), "".into())); + } + 1 => { + let _ = app.update(Message::SourceEdit( + iced::widget::text_editor::Action::Edit( + iced::widget::text_editor::Edit::Insert('?'), + ), + )); + } + _ => { + navigation_event(&mut app, |context| CanvasEvent::BeginOccurrence { + context, + tool: Tool::Action, + slot: Slot { + parent: context.scenario, + before: None, + }, + lifeline: None, + }); + form(&mut app, Event::Name(" Unfinished action ".into())); + } + } + let selected = app.selected; + let source = app.document.bundle().clone(); + let raw_source = app.source_editor.text(); + let property_snapshot = |app: &App| { + app.draft + .as_ref() + .map(|d| serde_json::to_value(d.recovery_snapshot()).unwrap()) + }; + let raw_properties = property_snapshot(&app); + let raw_behavior = app.behavior.scenario.recovery_snapshot().unwrap(); + navigation_event(&mut app, |ctx| { + if back { + CanvasEvent::Back(ctx) + } else { + CanvasEvent::OpenUse(ctx, first) + } + }); + assert_eq!(app.selected, selected); + assert_eq!(app.source_editor.text(), raw_source); + assert_eq!(property_snapshot(&app), raw_properties); + assert_eq!( + app.behavior.scenario.recovery_snapshot().unwrap(), + raw_behavior + ); + assert_eq!(app.document.bundle(), &source); + assert_eq!(app.project(), &p); + assert!(!app.document.can_undo()); + assert!(app.status.contains("Apply or cancel"), "{}", app.status); + if back { + assert_eq!(selected, b); + } + } + } + let mut app = App::with_project(p.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + for dimension in 0..4 { + let mut stale = app.scenario_context().unwrap(); + match dimension { + 0 => stale.document = Id::new_v4(), + 1 => stale.scenario = b, + 2 => stale.revision += 1, + _ => stale.epoch += 1, + } + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::OpenUse(stale, first))); + assert_eq!(app.selected, a); + assert!(app.behavior.scenario.return_to.is_empty()); + } + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, b)); + assert_eq!( + app.selected, a, + "a definition is not a local reference occurrence" + ); + } + #[test] + fn direct_scenario_reference_return_survives_rename_and_rejects_deleted_or_changed_use() { + let (p, [a, b, c], [first, _, _]) = navigation_fixture(); + for edit in 0..4 { + let mut app = App::with_project(p.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, first)); + let command = match edit { + 0 => Command::Batch(vec![ + Command::Rename(a, "Renamed caller".into()), + Command::Rename(first, "Renamed reference".into()), + ]), + 1 => Command::Delete(first), + 2 => Command::Delete(a), + _ => { + let mut changed = p.elements[&first].clone(); + if let Some(SharedBody::Occurrence { + body: OccurrenceBody::ScenarioUse { scenario, .. }, + .. + }) = changed.shared_mut() + { + *scenario = Some(c); + } + Command::EditElement(first, changed) + } + }; + let _ = app.update(Message::Command(command)); + let source = app.document.bundle().clone(); + let project = app.project().clone(); + let revision = app.document.revision(); + assert!(app.document.can_undo(), "{}", app.status); + navigation_event(&mut app, CanvasEvent::Back); + assert_eq!(app.selected, if edit == 0 { first } else { b }); + assert_eq!( + app.scenario_context().unwrap().scenario, + if edit == 0 { a } else { b }, + "Back must restore the caller workspace even if a rename already selected its UUID" + ); + assert_eq!(app.project(), &project); + assert_eq!(app.document.bundle(), &source); + assert_eq!(app.document.revision(), revision); + assert!(app.document.can_undo()); + assert!(!app.document.can_redo()); + } + } + #[test] + fn direct_clicked_occurrences_use_root_and_operand_slots_with_exact_source_history() { + let (f, branch, anchor, observed, reused, remote) = placement_fixture(); + for tool in [Tool::Action, Tool::Observation, Tool::Reuse, Tool::Fragment] { + for slot in [ + Slot { + parent: f.owner, + before: Some(f.reply), + }, + Slot { + parent: branch, + before: Some(anchor), + }, + ] { + for on_lifeline in [true, false] { + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Choose( + context, + CanvasTool::Add(tool), + ))); + let context = app.scenario_context().unwrap(); + let before = app.document.bundle().clone(); + let mut scene = Scene::sequence(app.project(), f.owner, f.call); + scene.interaction = Some((context, CanvasTool::Add(tool), true)); + scene.scroll_offset = iced::Vector::new(23., 80.); + let bounds = Rectangle::new(Point::new(20., 40.), scene.size); + let y = scene.slots.iter().find(|s| s.slot == slot).unwrap().y; + let x = if on_lifeline { + scene.lanes[&f.sender] + } else { + scene.size.width - 35. + }; + let point = Point::new(bounds.x + x, bounds.y + y); + let mut input = SequenceInput::default(); + canvas_mouse( + &scene, + &mut input, + mouse::Event::CursorMoved { + position: point - scene.scroll_offset, + }, + point, + bounds, + ); + assert!( + canvas_mouse( + &scene, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + bounds + ) + .is_none() + ); + let message = canvas_mouse( + &scene, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + point, + bounds, + ) + .unwrap(); + let line = (tool.uses_lifeline() && on_lifeline).then_some(f.sender); + assert!( + matches!(&message, Message::ScenarioCanvas(CanvasEvent::BeginOccurrence { tool: actual, slot: captured, lifeline, .. }) if *actual == tool && *captured == slot && *lifeline == line) + ); + assert!( + canvas_mouse( + &scene, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + point, + bounds + ) + .is_none(), + "one click publishes once" + ); + let _ = app.update(message); + assert_eq!( + app.behavior.scenario.pending_placement(f.owner), + Some((slot, line)) + ); + assert_eq!(app.document.bundle(), &before); + assert!(!app.document.can_undo()); + form(&mut app, Event::Name(format!("Placed {tool:?}"))); + match tool { + Tool::Action => form( + &mut app, + Event::Target(named_choice( + &f.project, + represented(&f.project, f.sender).unwrap(), + )), + ), + Tool::Observation => { + form(&mut app, Event::Target(named_choice(&f.project, observed))) + } + Tool::Reuse => { + form(&mut app, Event::Target(named_choice(&f.project, reused))); + form( + &mut app, + Event::Binding(remote, named_choice(&f.project, f.sender)), + ); + } + Tool::Fragment => form(&mut app, Event::Expression("ready".into())), + _ => unreachable!(), + } + if tool.uses_lifeline() && !on_lifeline { + form(&mut app, Event::Submit); + assert!( + app.behavior.has_pending(), + "off-lifeline creation requires an explicit lifeline" + ); + assert_eq!(app.document.bundle(), &before); + form(&mut app, Event::Sender(named_choice(&f.project, f.sender))); + } + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending(), "{tool:?}: {}", app.status); + let created = app.selected; + assert_eq!(app.project().elements[&created].parent, Some(slot.parent)); + let mut expected = f.project.elements[&slot.parent].steps().unwrap().clone(); + let index = expected + .iter() + .position(|id| Some(*id) == slot.before) + .unwrap(); + expected.insert(index, created); + assert_eq!( + app.project().elements[&slot.parent].steps().unwrap(), + &expected + ); + let untouched = if slot.parent == f.owner { + branch + } else { + f.owner + }; + assert_eq!( + app.project().elements[&untouched], + f.project.elements[&untouched] + ); + assert_eq!(app.project().relations, f.project.relations); + assert_eq!(app.project().elements[&reused], f.project.elements[&reused]); + assert_eq!( + language::parse(&language::format(app.project())).unwrap(), + *app.project() + ); + let after = app.document.bundle().clone(); + let result = app.project().clone(); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &before); + assert_eq!(app.project(), &f.project); + assert!(!app.document.can_undo()); + let _ = app.update(Message::Redo); + assert_eq!(app.document.bundle(), &after); + assert_eq!(app.project(), &result); + assert!(!app.document.can_redo()); + } + } + } + } + #[test] + fn direct_clicked_occurrence_rejects_stale_slots_and_retains_recoverable_drafts() { + let (f, branch, anchor, _, _, remote) = placement_fixture(); + let slot = Slot { + parent: branch, + before: Some(anchor), + }; + let mut state = State::default(); + for (tool, slot, line) in [ + (Tool::Lifeline, slot, None), + ( + Tool::Action, + Slot { + parent: f.owner, + before: Some(anchor), + }, + Some(f.sender), + ), + ( + Tool::Action, + Slot { + parent: Id::new_v4(), + before: None, + }, + Some(f.sender), + ), + (Tool::Action, slot, Some(remote)), + (Tool::Reuse, slot, Some(f.sender)), + ] { + assert!( + state + .begin_occurrence(&f.project, context(f.owner), tool, slot, line) + .is_err() + ); + assert!(state.draft.is_none()); + } + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let original = app.document.bundle().clone(); + let event = |context| CanvasEvent::BeginOccurrence { + context, + tool: Tool::Action, + slot, + lifeline: Some(f.sender), + }; + for dimension in 0..4 { + let mut stale = app.scenario_context().unwrap(); + match dimension { + 0 => stale.document = Id::new_v4(), + 1 => stale.scenario = Id::new_v4(), + 2 => stale.revision += 1, + _ => stale.epoch += 1, + } + let _ = app.update(Message::ScenarioCanvas(event(stale))); + assert!(!app.behavior.has_pending()); + } + let old_context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + old_context, + ScrollArea::Canvas, + iced::Vector::new(0., 100.), + ))); + let _ = app.update(Message::ScenarioCanvas(event(old_context))); + assert!(!app.behavior.has_pending()); + let current = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(event(current))); + form(&mut app, Event::Name(" Λ phase — retained ".into())); + let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap(); + form(&mut app, Event::Submit); + assert!( + app.behavior.has_pending(), + "missing action source rejects without losing the clicked slot" + ); + let _ = app.update(Message::Escape); + let current = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + current, + ScrollArea::Page, + iced::Vector::new(0., 200.), + ))); + assert_eq!( + app.behavior.scenario.recovery_snapshot().unwrap(), + Some(raw.clone()) + ); + assert_eq!(app.document.bundle(), &original); + assert!(!app.document.can_undo()); + let recovered = State::from_recovery(raw.clone()).unwrap(); + assert_eq!( + recovered.pending_placement(f.owner), + Some((slot, Some(f.sender))) + ); + assert_eq!(recovered.recovery_snapshot().unwrap(), Some(raw)); + assert!(recovered.prepare_pending(&f.project).is_err()); + form( + &mut app, + Event::Target(named_choice( + &f.project, + represented(&f.project, f.sender).unwrap(), + )), + ); + form(&mut app, Event::Submit); + assert!(!app.behavior.has_pending(), "{}", app.status); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &original); + } + #[test] + fn direct_middle_pair_insertion_is_one_exact_source_and_model_undo() { + let f = fixture(); + let mut state = State::default(); + let slot = Slot { + parent: f.owner, + before: Some(f.reply), + }; + state + .begin_message( + &f.project, + context(f.owner), + slot, + f.sender, + f.receiver, + MessageStyle::CallReply, + ) + .unwrap(); + assert!( + exchanges(&f.project, state.draft.as_ref().unwrap()) + .iter() + .any(|choice| choice.id == f.exchange) + ); + state + .update( + &f.project, + Event::Exchange(named_choice(&f.project, f.exchange)), + ) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().sender, Some(f.sender)); + assert_eq!(state.draft.as_ref().unwrap().receiver, Some(f.receiver)); + let mut document = + crate::document::Document::from_project(f.project.clone(), Default::default()).unwrap(); + let source = document.bundle().clone(); + document + .execute(state.prepare_pending(&f.project).unwrap()) + .unwrap(); + let p = document.project(); + let steps = p.elements[&f.owner].steps().unwrap(); + assert_eq!(steps.len(), 4); + assert_eq!((steps[0], steps[3]), (f.call, f.reply)); + assert!( + matches!(p.elements[&steps[2]].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { + reply_to: Some(call), message_kind: MessageKind::Reply, exchange: Some(exchange), .. }, .. }) if *call == steps[1] && *exchange == f.exchange) + ); + assert_eq!(p.relations, f.project.relations); + assert_eq!(language::parse(&language::format(p)).unwrap(), *p); + assert!(document.undo()); + assert_eq!(document.project(), &f.project); + assert_eq!(document.bundle(), &source); + assert!(!document.undo()); + } + #[test] + fn direct_reply_choices_validate_the_actual_slot_and_reorder_rejects_reverse_order() { + let f = fixture(); + let p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap(); + let mut state = State::default(); + state + .begin_message( + &p, + context(f.owner), + Slot { + parent: f.owner, + before: Some(f.call), + }, + f.receiver, + f.sender, + MessageStyle::Reply, + ) + .unwrap(); + assert!(calls(&p, state.draft.as_ref().unwrap()).is_empty()); + state.update(&p, Event::AppendInstead).unwrap(); + assert!( + calls(&p, state.draft.as_ref().unwrap()) + .iter() + .any(|choice| choice.id == f.call) + ); + state + .update(&p, Event::Call(named_choice(&p, f.call))) + .unwrap(); + let result = execute_batch(&p, &[state.prepare_pending(&p).unwrap()]).unwrap(); + let reply = *result.elements[&f.owner].steps().unwrap().last().unwrap(); + assert!( + reorder( + &result, + f.owner, + reply, + Slot { + parent: f.owner, + before: Some(f.call) + } + ) + .unwrap_err() + .contains("call") + ); + assert_eq!(result.elements[&f.owner].steps().unwrap(), &[f.call, reply]); + } + #[test] + fn direct_operand_pair_keeps_parent_sequence_and_cannot_reorder_across_operands() { + let f = fixture(); + let mut state = State::default(); + state + .update(&f.project, Event::Start(f.owner, Tool::Fragment)) + .unwrap(); + state + .update(&f.project, Event::Expression("ready".into())) + .unwrap(); + let p = execute_batch(&f.project, &[state.prepare_pending(&f.project).unwrap()]).unwrap(); + let fragment = *p.elements[&f.owner].steps().unwrap().last().unwrap(); + let branches: Vec<_> = p.children(fragment).map(|e| e.id).collect(); + let root_steps = p.elements[&f.owner].steps().unwrap().clone(); + let mut state = State::default(); + state + .begin_message( + &p, + context(f.owner), + Slot { + parent: branches[0], + before: None, + }, + f.sender, + f.receiver, + MessageStyle::CallReply, + ) + .unwrap(); + state + .update(&p, Event::Exchange(named_choice(&p, f.exchange))) + .unwrap(); + let result = execute_batch(&p, &[state.prepare_pending(&p).unwrap()]).unwrap(); + assert_eq!(result.elements[&f.owner].steps().unwrap(), &root_steps); + let branch_steps = result.elements[&branches[0]].steps().unwrap(); + assert_eq!(branch_steps.len(), 2); + assert!( + reorder( + &result, + f.owner, + branch_steps[0], + Slot { + parent: branches[1], + before: None + } + ) + .is_err() + ); + } + #[test] + fn direct_busy_save_keeps_observed_scroll_current_for_the_next_pointer_gesture() { + let f = fixture(); + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let context = app.scenario_context().unwrap(); + let original = app.document.snapshot(); + app.busy = true; + for (area, offset) in [ + (ScrollArea::Canvas, iced::Vector::new(25., 100.)), + (ScrollArea::Page, iced::Vector::new(0., 40.)), + ] { + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + context, area, offset, + ))); + } + assert!(app.busy); + assert_eq!( + app.behavior.scenario.scroll_offset(), + iced::Vector::new(25., 100.) + ); + assert_ne!( + app.scenario_context(), + Some(context), + "scroll invalidates the pre-save pointer context even while busy" + ); + let mut stale = context; + stale.document = Id::new_v4(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Scrolled( + stale, + ScrollArea::Canvas, + iced::Vector::new(0., 900.), + ))); + assert_eq!( + app.behavior.scenario.scroll_offset(), + iced::Vector::new(25., 100.) + ); + let root = std::path::PathBuf::from("scenario-scroll-save-fixture"); + let receipt = crate::app::FileSnapshot { + document: context.document, + revision: context.revision, + snapshot: original.clone(), + path: root.join(&original.bundle.entry), + disk: Some(oa_core::source_files::DiskProject { + root, + bundle: original.bundle.clone(), + expectations: BTreeMap::new(), + reachable: original.bundle.files.keys().cloned().collect(), + diagnostics: vec![], + }), + }; + let _ = app.update(Message::Saved(Ok(Some(receipt)))); + assert!(!app.busy); + assert_eq!(app.project(), &original.decoded.project); + assert!(!app.document.can_undo()); + let context = app.scenario_context().unwrap(); + let mut scene = Scene::sequence(app.project(), f.owner, f.call); + scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true)); + scene.scroll_offset = app.behavior.scenario.scroll_offset(); + let slot = scene.slots.last().unwrap().slot; + let bounds = Rectangle::new(Point::new(20., 250.), scene.size); + let y = bounds.y + scene.slots.last().unwrap().y; + let a = Point::new(bounds.x + scene.lanes[&f.sender], y) - scene.scroll_offset; + let b = Point::new(bounds.x + scene.lanes[&f.receiver], y) - scene.scroll_offset; + let mut input = super::super::SequenceInput::default(); + let mut published = vec![]; + for event in [ + mouse::Event::CursorMoved { position: a }, + mouse::Event::ButtonPressed(mouse::Button::Left), + mouse::Event::CursorMoved { position: b }, + mouse::Event::ButtonReleased(mouse::Button::Left), + ] { + if let Some(message) = canvas::Program::update( + &scene, + &mut input, + &canvas::Event::Mouse(event), + bounds, + mouse::Cursor::Available(b + scene.scroll_offset), + ) + .and_then(|action| action.into_inner().0) + { + published.push(message); + } + } + assert_eq!(published.len(), 1); + assert!( + matches!(&published[0], Message::ScenarioCanvas(CanvasEvent::Begin { sender, receiver, slot: actual, .. }) if *sender == f.sender && *receiver == f.receiver && *actual == slot) + ); + let _ = app.update(published.pop().unwrap()); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().before, + slot.before + ); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().sender, + Some(f.sender) + ); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().receiver, + Some(f.receiver) + ); + } + #[test] + fn direct_reply_choices_cache_keeps_typing_fast_and_revalidates_changed_contexts() { + let f = fixture(); + let mut p = execute_batch(&f.project, &[Command::Delete(f.reply)]).unwrap(); + let mut commands = vec![]; + for index in 1..100 { + commands.push(Command::CreateElement(occurrence( + &p, + f.owner, + &format!("Call {index}"), + OccurrenceBody::Exchange { + operation: None, + exchange: Some(f.exchange), + sender: Some(f.sender), + receiver: Some(f.receiver), + message_kind: MessageKind::Call, + reply_to: None, + values: vec![], + }, + ))); + } + p = execute_batch(&p, &commands).unwrap(); + let mut state = State::default(); + let mut context = context(f.owner); + state + .begin_message( + &p, + context, + Slot { + parent: f.owner, + before: None, + }, + f.receiver, + f.sender, + MessageStyle::Reply, + ) + .unwrap(); + let start = std::time::Instant::now(); + let expected: BTreeSet<_> = state + .call_choices(&p, context, state.draft.as_ref().unwrap()) + .into_iter() + .map(|c| c.id) + .collect(); + assert_eq!(expected.len(), 100); + eprintln!( + "100 exact-slot earlier-call choices cold: {:?}", + start.elapsed() + ); + for event in [ + Event::Name("unfinished name".into()), + Event::ChoiceSearch("Call".into()), + Event::ChoicePage(2), + Event::Value( + crate::data_values::FieldPathKey { + path: vec![], + field: Id::new_v4(), + }, + "unfinished expression".into(), + ), + ] { + state.update(&p, event).unwrap(); + let start = std::time::Instant::now(); + assert_eq!( + state + .call_choices(&p, context, state.draft.as_ref().unwrap()) + .into_iter() + .map(|c| c.id) + .collect::>(), + expected + ); + eprintln!( + "100 earlier-call choices after draft edit: {:?}", + start.elapsed() + ); + } + state.draft.as_mut().unwrap().before = Some(f.call); + assert!( + state + .call_choices(&p, context, state.draft.as_ref().unwrap()) + .is_empty(), + "changed insertion invalidates cached reply order" + ); + state.update(&p, Event::AppendInstead).unwrap(); + assert_eq!( + state + .call_choices(&p, context, state.draft.as_ref().unwrap()) + .len(), + 100 + ); + let deleted = *expected.iter().find(|id| **id != f.call).unwrap(); + p = execute_batch(&p, &[Command::Delete(deleted)]).unwrap(); + context.revision += 1; + assert_eq!( + state + .call_choices(&p, context, state.draft.as_ref().unwrap()) + .len(), + 99 + ); + let deleted = *expected + .iter() + .find(|id| **id != f.call && **id != deleted) + .unwrap(); + p = execute_batch(&p, &[Command::Delete(deleted)]).unwrap(); + context.document = Id::new_v4(); + assert_eq!( + state + .call_choices(&p, context, state.draft.as_ref().unwrap()) + .len(), + 98, + "another document with the same IDs and revision still invalidates cached choices" + ); + } + #[test] + fn direct_exchange_compatibility_browsing_preserves_final_slot_validation() { + let f = fixture(); + let mut state = State::default(); + state + .begin_message( + &f.project, + context(f.owner), + Slot { + parent: f.owner, + before: Some(f.call), + }, + f.sender, + f.receiver, + MessageStyle::Delete, + ) + .unwrap(); + assert!( + exchanges(&f.project, state.draft.as_ref().unwrap()) + .iter() + .any(|c| c.id == f.exchange) + ); + state + .update( + &f.project, + Event::Exchange(named_choice(&f.project, f.exchange)), + ) + .unwrap(); + assert!( + state.prepare_pending(&f.project).is_err(), + "deleting the callee before the existing call fails exact-slot lifecycle validation" + ); + state.update(&f.project, Event::AppendInstead).unwrap(); + assert!(state.prepare_pending(&f.project).is_ok()); + state.draft.as_mut().unwrap().sender = Some(f.receiver); + state.draft.as_mut().unwrap().receiver = Some(f.sender); + assert!( + !exchanges(&f.project, state.draft.as_ref().unwrap()) + .iter() + .any(|c| c.id == f.exchange), + "reversed represented endpoints are not offered" + ); + } + #[test] + fn direct_five_hundred_exchange_choice_render_probe() { + let mut f = fixture(); + let template = f.project.relations[&f.exchange].clone(); + for index in 1..500 { + let id = f.project.relate( + RelationKind::Interaction, + &format!("Contract {index:03}"), + template.source(), + template.target(), + ); + f.project.relations.get_mut(&id).unwrap().body = template.body.clone(); + } + let mut state = State::default(); + state + .begin_message( + &f.project, + context(f.owner), + Slot { + parent: f.owner, + before: None, + }, + f.sender, + f.receiver, + MessageStyle::CallReply, + ) + .unwrap(); + let started = std::time::Instant::now(); + assert_eq!( + exchanges(&f.project, state.draft.as_ref().unwrap()).len(), + 500 + ); + eprintln!("500 compatible exchange choices: {:?}", started.elapsed()); + state + .update(&f.project, Event::Name("Draft name changed".into())) + .unwrap(); + let started = std::time::Instant::now(); + assert_eq!( + exchanges(&f.project, state.draft.as_ref().unwrap()).len(), + 500 + ); + eprintln!("500 choices after name keystroke: {:?}", started.elapsed()); + } + #[test] + fn direct_exchange_picker_is_bounded_and_browsing_keeps_pending_values() { + let mut f = fixture(); + let template = f.project.relations[&f.exchange].clone(); + for index in 0..70 { + let id = f.project.relate( + RelationKind::Interaction, + &format!("Other contract {index:02}"), + template.source(), + template.target(), + ); + f.project.relations.get_mut(&id).unwrap().body = template.body.clone(); + } + let mut state = State::default(); + state + .begin_message( + &f.project, + context(f.owner), + Slot { + parent: f.owner, + before: None, + }, + f.sender, + f.receiver, + MessageStyle::CallReply, + ) + .unwrap(); + state + .update( + &f.project, + Event::Exchange(named_choice(&f.project, f.exchange)), + ) + .unwrap(); + let key = crate::data_values::roots( + &f.project, + &f.project.relations[&f.exchange], + &MessageKind::Call, + )[0] + .key + .clone(); + state + .update(&f.project, Event::Value(key.clone(), "pending(".into())) + .unwrap(); + let choices = exchanges(&f.project, state.draft.as_ref().unwrap()); + assert_eq!(choices.len(), 71); + let mut visited = BTreeSet::new(); + for page in 0..choices.len().div_ceil(VALUE_PAGE_SIZE) { + state.update(&f.project, Event::ChoicePage(page)).unwrap(); + let d = state.draft.as_ref().unwrap(); + for choice in &choices[value_page(choices.len(), d.choice_page)] { + visited.insert(choice.id); + } + let view = bounded_picker(choices.clone(), d, d.exchange, "Choose", Event::Exchange); + let tree = iced::advanced::widget::Tree::new(view.as_widget()); + fn count(tree: &iced::advanced::widget::Tree) -> usize { + 1 + tree.children.iter().map(count).sum::() + } + assert!(count(&tree) < 20); + } + assert_eq!(visited.len(), 71); + state + .update(&f.project, Event::ChoiceSearch("Other contract 69".into())) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().choice_page, 0); + assert_eq!(state.draft.as_ref().unwrap().values[&key], "pending("); + assert_eq!(state.draft.as_ref().unwrap().exchange, Some(f.exchange)); + } + #[test] + fn direct_form_keeps_the_same_canvas_in_the_widget_tree() { + let f = fixture(); + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let canvas: Element<'_, Message> = + canvas::Canvas::new(Scene::sequence(app.project(), f.owner, f.call)).into(); + let tag = iced::advanced::widget::Tree::new(canvas.as_widget()).tag; + fn count( + tree: &iced::advanced::widget::Tree, + tag: iced::advanced::widget::tree::Tag, + ) -> usize { + usize::from(tree.tag == tag) + + tree + .children + .iter() + .map(|child| count(child, tag)) + .sum::() + } + for pending in [false, true] { + if pending { + let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Begin { + context, + slot: Slot { + parent: f.owner, + before: None, + }, + sender: f.sender, + receiver: f.receiver, + style: MessageStyle::CallReply, + })); + assert!(app.behavior.has_pending()); + } + let view = view(&app, f.owner, Column::new()); + let tree = iced::advanced::widget::Tree::new(view.as_widget()); + assert_eq!(count(&tree, tag), 1); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn reverse_communication_canvas_choices_and_correlated_reply_share_core_direction() { + let mut p = Project::empty(); + let a = p + .add_element(Kind::Entity, Layer::Oa, "Crew", None) + .unwrap(); + let b = p + .add_element(Kind::Actor, Layer::Oa, "Station", None) + .unwrap(); + let exchange = p.relate(RelationKind::CommunicationMean, "Radio", a, b); + let owner = scenario(&mut p, "Participant dialogue"); + if let Some(SharedBody::Scenario { viewpoint, .. }) = + p.elements.get_mut(&owner).unwrap().shared_mut() + { + *viewpoint = ScenarioViewpoint::ParticipantInteraction; + } + let la = lifeline(&mut p, owner, a); + let lb = lifeline(&mut p, owner, b); + let mut state = State::default(); + state + .begin_message( + &p, + direct_tests::context(owner), + Slot { + parent: owner, + before: None, + }, + lb, + la, + MessageStyle::CallReply, + ) + .unwrap(); + assert!( + exchanges(&p, state.draft.as_ref().unwrap()) + .iter() + .any(|c| c.id == exchange) + ); + state + .update(&p, Event::Exchange(named_choice(&p, exchange))) + .unwrap(); + let mut document = + crate::document::Document::from_project(p.clone(), Default::default()).unwrap(); + let original = document.bundle().clone(); + document + .execute(state.prepare_pending(&p).unwrap()) + .unwrap(); + let accepted = document.project(); + let steps = accepted.elements[&owner].steps().unwrap(); + assert_eq!(steps.len(), 2); + for (id, expected) in [(steps[0], (b, a)), (steps[1], (a, b))] { + let Some(SharedBody::Occurrence { body, .. }) = accepted.elements[&id].shared() else { + unreachable!() + }; + assert_eq!( + validation::message_endpoints(accepted, body), + Some(expected) + ); + } + let after = document.bundle().clone(); + assert!(document.undo()); + assert_eq!(document.bundle(), &original); + assert!(document.redo()); + assert_eq!(document.bundle(), &after); + let mut directed = p.clone(); + if let RelationBody::CommunicationMean { oriented, .. } = + &mut directed.relations.get_mut(&exchange).unwrap().body + { + *oriented = true; + } + assert!( + !exchanges(&directed, state.draft.as_ref().unwrap()) + .iter() + .any(|c| c.id == exchange) + ); + } + fn scenario(p: &mut Project, name: &str) -> Id { + let mut e = p + .make_element(Kind::Scenario, Layer::Oa, name, None) + .unwrap(); + let id = e.id; + if let Some(SharedBody::Scenario { viewpoint, .. }) = e.shared_mut() { + *viewpoint = ScenarioViewpoint::OperationalActivity; + } + *p = execute_batch(p, &[Command::CreateElement(e)]).unwrap(); + id + } + fn lifeline(p: &mut Project, owner: Id, represented: Id) -> Id { + let mut state = State::default(); + state + .update(p, Event::Start(owner, Tool::Lifeline)) + .unwrap(); + state + .update(p, Event::Target(named_choice(p, represented))) + .unwrap(); + let command = state.update(p, Event::Submit).unwrap().unwrap(); + *p = execute_batch(p, &[command]).unwrap(); + p.children(owner).find(|e|matches!(e.shared(),Some(SharedBody::Lifeline {represented:Some(id)}) if *id==represented)).unwrap().id + } + fn fixture() -> (Project, Id, Id, Id, Id) { + let mut p = Project::demo(); + let relation = p + .relations + .values() + .find(|r| r.kind() == RelationKind::Interaction) + .unwrap(); + let (exchange, a, b) = (relation.id, relation.source(), relation.target()); + let owner = scenario(&mut p, "Scenario authoring test"); + let sender = lifeline(&mut p, owner, a); + let receiver = lifeline(&mut p, owner, b); + (p, owner, exchange, sender, receiver) + } + fn message(p: &Project, owner: Id, exchange: Id, sender: Id, receiver: Id) -> State { + let mut state = State::default(); + state.update(p, Event::Start(owner, Tool::Message)).unwrap(); + state + .update(p, Event::Exchange(named_choice(p, exchange))) + .unwrap(); + state + .update(p, Event::Sender(named_choice(p, sender))) + .unwrap(); + state + .update(p, Event::Receiver(named_choice(p, receiver))) + .unwrap(); + state + } + #[test] + fn graphical_call_reply_is_atomic_correlated_and_source_equivalent() { + let (p, owner, exchange, sender, receiver) = fixture(); + let mut state = message(&p, owner, exchange, sender, receiver); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + let mut session = Session::new(p.clone()); + session.execute(command).unwrap(); + let result = session.project(); + let steps = result.elements[&owner].steps().unwrap(); + assert_eq!(steps.len(), 2); + assert!( + matches!(result.elements[&steps[1]].shared(),Some(SharedBody::Occurrence {body:OccurrenceBody::Exchange {message_kind:MessageKind::Reply,reply_to:Some(call),sender:Some(from),receiver:Some(to),..},..}) if *call==steps[0]&&*from==receiver&&*to==sender) + ); + assert_eq!(language::parse(&language::format(result)).unwrap(), *result); + let scene = Scene::sequence(result, owner, owner); + assert!( + scene + .lines + .iter() + .any(|l| l.id == steps[1] && l.dashed && l.label.contains("reply")) + ); + assert!( + scene + .boxes + .iter() + .any(|b| b.id == steps[0] && b.rect.width == 8.) + ); + let mut reply = State::default(); + reply + .update(result, Event::Start(owner, Tool::Message)) + .unwrap(); + reply + .update(result, Event::Style(MessageStyle::Reply)) + .unwrap(); + assert!(calls(result, reply.draft.as_ref().unwrap()).is_empty()); + session.undo(); + assert_eq!(session.project(), &p); + } + #[test] + fn scenario_reuse_requires_explicit_compatible_bindings() { + let (mut p, owner, _, sender, receiver) = fixture(); + let a = represented(&p, sender).unwrap(); + let b = represented(&p, receiver).unwrap(); + let target = scenario(&mut p, "Reusable scenario"); + let remote_a = lifeline(&mut p, target, a); + let remote_b = lifeline(&mut p, target, b); + let mut state = State::default(); + state.update(&p, Event::Start(owner, Tool::Reuse)).unwrap(); + state + .update(&p, Event::Target(named_choice(&p, target))) + .unwrap(); + assert!(state.update(&p, Event::Submit).is_err()); + assert!(state.draft.is_some()); + state + .update(&p, Event::Binding(remote_a, named_choice(&p, receiver))) + .unwrap(); + state + .update(&p, Event::Binding(remote_b, named_choice(&p, sender))) + .unwrap(); + assert!(state.update(&p, Event::Submit).is_err()); + assert!(state.draft.is_some()); + state + .update(&p, Event::Binding(remote_a, named_choice(&p, sender))) + .unwrap(); + state + .update(&p, Event::Binding(remote_b, named_choice(&p, receiver))) + .unwrap(); + let result = + execute_batch(&p, &[state.update(&p, Event::Submit).unwrap().unwrap()]).unwrap(); + let use_id = result.elements[&owner].steps().unwrap()[0]; + assert!( + matches!(result.elements[&use_id].shared(),Some(SharedBody::Occurrence {body:OccurrenceBody::ScenarioUse {scenario:Some(id),bindings},..}) if *id==target&&bindings.len()==2) + ); + assert_eq!(result.children(target).count(), 2); + assert_eq!(language::parse(&language::format(&result)).unwrap(), result); + } + #[test] + fn fragment_operands_accept_paired_messages_through_normal_destination_picker() { + let (mut p, owner, exchange, sender, receiver) = fixture(); + let mut state = State::default(); + state + .update(&p, Event::Start(owner, Tool::Fragment)) + .unwrap(); + state + .update(&p, Event::Name("Choose outcome".into())) + .unwrap(); + state + .update(&p, Event::Expression("accepted".into())) + .unwrap(); + p = execute_batch(&p, &[state.update(&p, Event::Submit).unwrap().unwrap()]).unwrap(); + let fragment = p.elements[&owner].steps().unwrap()[0]; + let branches = p.children(fragment).map(|e| e.id).collect::>(); + assert_eq!(branches.len(), 2); + let mut state = message(&p, owner, exchange, sender, receiver); + state + .update(&p, Event::Parent(named_choice(&p, branches[0]))) + .unwrap(); + p = execute_batch(&p, &[state.update(&p, Event::Submit).unwrap().unwrap()]).unwrap(); + assert_eq!(p.elements[&branches[0]].steps().unwrap().len(), 2); + assert_eq!( + Scene::sequence(&p, owner, owner) + .lines + .iter() + .filter(|l| !l.label.is_empty()) + .count(), + 2 + ); + } + #[test] + fn invalid_lifecycle_edit_keeps_draft_and_timer_has_no_global_exchange() { + let (mut p, owner, exchange, sender, receiver) = fixture(); + let mut state = message(&p, owner, exchange, sender, receiver); + p = execute_batch(&p, &[state.update(&p, Event::Submit).unwrap().unwrap()]).unwrap(); + let mut state = message(&p, owner, exchange, sender, receiver); + state.draft.as_mut().unwrap().style = MessageStyle::Create; + assert!(state.update(&p, Event::Submit).is_err()); + assert!(state.draft.is_some()); + state.update(&p, Event::Style(MessageStyle::Timer)).unwrap(); + state + .update(&p, Event::Sender(named_choice(&p, sender))) + .unwrap(); + state.update(&p, Event::Expression("30 s".into())).unwrap(); + p = execute_batch(&p, &[state.update(&p, Event::Submit).unwrap().unwrap()]).unwrap(); + let timer = *p.elements[&owner].steps().unwrap().last().unwrap(); + assert!( + matches!(p.elements[&timer].shared(),Some(SharedBody::Occurrence {body:OccurrenceBody::Exchange {exchange:None,receiver:None,message_kind:MessageKind::Timer {expression},..},..}) if expression=="30 s") + ); + assert!( + Scene::sequence(&p, owner, owner) + .lines + .iter() + .any(|l| l.id == timer && l.label.contains("30 s")) + ); + } + #[test] + fn tagged_scenario_commit_is_not_redirected_by_other_editor_events() { + let (p, owner, exchange, sender, receiver) = fixture(); + let mut state = crate::behavior::State::default(); + for e in [ + Event::Start(owner, Tool::Message), + Event::Exchange(named_choice(&p, exchange)), + Event::Sender(named_choice(&p, sender)), + Event::Receiver(named_choice(&p, receiver)), + ] { + state + .update(&p, crate::behavior::Event::Scenario(e)) + .unwrap(); + } + state + .update( + &p, + crate::behavior::Event::Topology(super::super::topology::Event::Cancel), + ) + .unwrap(); + let submit = crate::behavior::Event::Scenario(Event::Submit); + assert!(submit.is_submit()); + let command = state.update(&p, submit).unwrap().unwrap(); + let result = execute_batch(&p, &[command]).unwrap(); + assert_eq!(result.elements[&owner].steps().unwrap().len(), 2); + } + #[test] + fn lifeline_picker_uses_core_viewpoint_contract() { + let (p, owner, ..) = fixture(); + let mut state = State::default(); + state + .update(&p, Event::Start(owner, Tool::Lifeline)) + .unwrap(); + let choices = sources(&p, state.draft.as_ref().unwrap()); + assert!(!choices.is_empty()); + assert!( + choices + .iter() + .all(|c| p.elements[&c.id].kind() == Kind::Activity) + ); + } +} + +#[cfg(test)] +mod data_contract_tests { + use super::*; + + #[test] + fn scenario_form_commits_two_reused_record_values_and_correlated_reply_atomically() { + let mut f = crate::data_values::tests::payload(); + if let Some(SharedBody::DataField { direction, .. }) = f + .project + .elements + .get_mut(&f.destination) + .unwrap() + .shared_mut() + { + *direction = ParameterDirection::InOut; + } + let source = f.project.relations[&f.exchange].source(); + let target = f.project.relations[&f.exchange].target(); + let mut lines = vec![]; + for (name, represented) in [("Requester", source), ("Performer", target)] { + let id = f + .project + .add_element(Kind::Lifeline, Layer::Oa, name, Some(f.scenario)) + .unwrap(); + *f.project + .elements + .get_mut(&id) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Lifeline { + represented: Some(represented), + }; + lines.push(id); + } + let mut state = State::default(); + for event in [ + Event::Start(f.scenario, Tool::Message), + Event::Name("Move coordinates".into()), + Event::Exchange(named_choice(&f.project, f.exchange)), + Event::Sender(named_choice(&f.project, lines[0])), + Event::Receiver(named_choice(&f.project, lines[1])), + ] { + state.update(&f.project, event).unwrap(); + } + assert!( + exchanges(&f.project, state.draft.as_ref().unwrap()) + .iter() + .any(|c| c.id == f.exchange) + ); + for (parameter, expression) in [(f.origin, "45.0"), (f.destination, "46.0")] { + state + .update( + &f.project, + Event::Value( + crate::data_values::FieldPathKey { + path: vec![parameter], + field: f.leaf, + }, + expression.into(), + ), + ) + .unwrap(); + } + let before = state.draft.as_ref().unwrap().values.clone(); + state + .update(&f.project, Event::Style(MessageStyle::Call)) + .unwrap(); + state + .update( + &f.project, + Event::Exchange(named_choice(&f.project, f.exchange)), + ) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().values, before); + assert_eq!(state.draft.as_ref().unwrap().sender, Some(lines[0])); + assert_eq!(state.draft.as_ref().unwrap().receiver, Some(lines[1])); + state + .update(&f.project, Event::Style(MessageStyle::CallReply)) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().values, before); + let command = state.prepare_pending(&f.project).unwrap(); + let mut document = + crate::document::Document::from_project(f.project.clone(), Default::default()).unwrap(); + document.execute(command).unwrap(); + let messages = document + .project() + .children(f.scenario) + .filter_map(|e| match e.shared()? { + SharedBody::Occurrence { + body: + OccurrenceBody::Exchange { + message_kind, + values, + .. + }, + .. + } => Some((message_kind, values)), + _ => None, + }) + .collect::>(); + let call = messages + .iter() + .find(|(kind, _)| **kind == MessageKind::Call) + .unwrap() + .1; + assert_eq!(call.len(), 2); + assert_eq!( + call.iter().map(|v| v.field).collect::>(), + BTreeSet::from([f.leaf]) + ); + assert_eq!( + call.iter().map(|v| v.path.clone()).collect::>(), + BTreeSet::from([vec![f.origin], vec![f.destination]]) + ); + assert!( + messages + .iter() + .find(|(kind, _)| **kind == MessageKind::Reply) + .unwrap() + .1 + .is_empty() + ); + assert!(document.undo()); + assert_eq!(document.project(), &f.project); + assert!(!document.undo()); + } + #[test] + fn recursive_value_focus_exposes_authored_leaf_and_phase_changes_retain_surviving_keys() { + let (f, next) = crate::data_values::tests::recursive_payload(); + let mut state = State::default(); + state + .update(&f.project, Event::Start(f.scenario, Tool::Message)) + .unwrap(); + state + .update( + &f.project, + Event::Exchange(named_choice(&f.project, f.exchange)), + ) + .unwrap(); + let key = crate::data_values::FieldPathKey { + path: vec![f.origin, next], + field: f.leaf, + }; + state + .update( + &f.project, + Event::ValueBrowse(crate::data_values::parent(&key)), + ) + .unwrap(); + state + .update(&f.project, Event::ValueFocus(key.clone())) + .unwrap(); + assert!( + state + .draft + .as_ref() + .unwrap() + .browsed_values(&f.project, &f.project.relations[&f.exchange]) + .unwrap() + .iter() + .any(|c| c.key == key) + ); + state + .update( + &f.project, + Event::Value(key.clone(), "recursive expression".into()), + ) + .unwrap(); + state + .update(&f.project, Event::Style(MessageStyle::Call)) + .unwrap(); + assert_eq!( + state.draft.as_ref().unwrap().values[&key], + "recursive expression" + ); + state + .update(&f.project, Event::Style(MessageStyle::Asynchronous)) + .unwrap(); + assert_eq!( + state.draft.as_ref().unwrap().values[&key], + "recursive expression" + ); + state + .update(&f.project, Event::Style(MessageStyle::Reply)) + .unwrap(); + assert!( + state.draft.as_ref().unwrap().values.is_empty(), + "In parameters do not become reply values" + ); + let out = crate::data_values::FieldPathKey { + path: vec![f.destination], + field: f.leaf, + }; + state + .update(&f.project, Event::Value(out.clone(), "reply value".into())) + .unwrap(); + state + .update(&f.project, Event::Style(MessageStyle::Asynchronous)) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().values[&out], "reply value"); + } + + fn panel_counts(p: &Project, d: &Draft, exchange: Id) -> (usize, usize) { + let input: Element<'_, Message> = text_input("", "").into(); + let input_tag = iced::advanced::widget::Tree::new(input.as_widget()).tag; + let panel = value_panel(p, d, &p.relations[&exchange]); + let tree = iced::advanced::widget::Tree::new(panel.as_widget()); + fn count( + tree: &iced::advanced::widget::Tree, + tag: iced::advanced::widget::tree::Tag, + ) -> (usize, usize) { + tree.children.iter().fold( + (1, usize::from(tree.tag == tag)), + |(nodes, inputs), child| { + let (child_nodes, child_inputs) = count(child, tag); + (nodes + child_nodes, inputs + child_inputs) + }, + ) + } + count(&tree, input_tag) + } + + #[test] + fn large_payload_has_one_editor_with_reachable_field_and_authored_pages() { + let f = crate::data_values::tests::wide_payload(); + let p = &f.project; + let exchange = &p.relations[&f.exchange]; + let mut state = State::default(); + state + .update(p, Event::Start(f.scenario, Tool::Message)) + .unwrap(); + state + .update(p, Event::Exchange(named_choice(p, f.exchange))) + .unwrap(); + let roots = crate::data_values::roots(p, exchange, &MessageKind::Call); + assert_eq!(roots.len(), 96); + assert_eq!( + panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1, + 0 + ); + let mut visited = BTreeSet::new(); + for page in 0..roots.len().div_ceil(VALUE_PAGE_SIZE) { + state + .update(p, Event::ValuePage(ValueSection::Fields, page)) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + let choices = d.browsed_values(p, exchange).unwrap(); + let visible = choices[value_page(choices.len(), d.value_browse_page)].to_vec(); + assert!(visible.len() <= VALUE_PAGE_SIZE); + for choice in visible { + visited.insert(choice.key.clone()); + state + .update(p, Event::ValueFocus(choice.key.clone())) + .unwrap(); + state + .update(p, Event::Value(choice.key, format!("draft {page}"))) + .unwrap(); + } + let (nodes, inputs) = panel_counts(p, state.draft.as_ref().unwrap(), f.exchange); + assert_eq!( + inputs, 1, + "one input for over twelve thousand carried paths" + ); + assert!(nodes < 200, "{nodes} widgets for the bounded value panel"); + } + assert_eq!( + visited, + roots.iter().map(|choice| choice.key.clone()).collect() + ); + + state + .update(p, Event::ValueBrowse(Some(roots[0].key.clone()))) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().value_browse_page, 0); + let children = state + .draft + .as_ref() + .unwrap() + .browsed_values(p, exchange) + .unwrap(); + assert_eq!(children.len(), 128); + let mut visited_children = BTreeSet::new(); + for page in 0..children.len().div_ceil(VALUE_PAGE_SIZE) { + state + .update(p, Event::ValuePage(ValueSection::Fields, page)) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + let choices = d.browsed_values(p, exchange).unwrap(); + for choice in &choices[value_page(choices.len(), d.value_browse_page)] { + assert_eq!(choice.key.path, [roots[0].key.field]); + visited_children.insert(choice.key.clone()); + } + } + assert_eq!( + visited_children, + children.iter().map(|choice| choice.key.clone()).collect() + ); + + let nested = children[0].key.clone(); + state.update(p, Event::ValueFocus(nested.clone())).unwrap(); + state + .update(p, Event::Value(nested.clone(), "unfinished(\"".into())) + .unwrap(); + let before = state.draft.as_ref().unwrap().values.clone(); + let before_browse = state.draft.as_ref().unwrap().value_browse.clone(); + let before_page = state.draft.as_ref().unwrap().value_browse_page; + for event in [ + Event::Style(MessageStyle::Call), + Event::Exchange(named_choice(p, f.exchange)), + Event::Style(MessageStyle::CallReply), + ] { + state.update(p, event).unwrap(); + } + let d = state.draft.as_ref().unwrap(); + assert_eq!(d.values, before); + assert_eq!(d.value_browse, before_browse); + assert_eq!(d.value_browse_page, before_page); + assert_eq!(d.value_focus, Some(nested)); + + let mut authored = BTreeSet::new(); + for page in 0..before.len().div_ceil(VALUE_PAGE_SIZE) { + state + .update(p, Event::ValuePage(ValueSection::Authored, page)) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + let values = d.authored_values(); + for (key, _) in &values[value_page(values.len(), d.value_authored_page)] { + authored.insert((*key).clone()); + } + let (nodes, inputs) = panel_counts(p, d, f.exchange); + assert_eq!(inputs, 1); + assert!(nodes < 200, "{nodes} widget nodes"); + } + assert_eq!(authored, before.keys().cloned().collect()); + state.update(p, Event::ValueBrowse(None)).unwrap(); + state + .update(p, Event::ValuePage(ValueSection::Fields, usize::MAX)) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + assert_eq!( + d.values, before, + "browsing and page clamping never remove drafts" + ); + let OccurrenceBody::Exchange { values, .. } = d.message_body() else { + panic!() + }; + assert_eq!( + values.len(), + before.len(), + "commit includes every authored page" + ); + assert_eq!(panel_counts(p, d, f.exchange).1, 1); + } + + #[test] + fn recursive_browsing_up_and_root_keep_values_and_report_the_depth_limit() { + let (f, next) = crate::data_values::tests::recursive_payload(); + let p = &f.project; + let exchange = &p.relations[&f.exchange]; + let mut state = State::default(); + state + .update(p, Event::Start(f.scenario, Tool::Message)) + .unwrap(); + state + .update(p, Event::Exchange(named_choice(p, f.exchange))) + .unwrap(); + let root = crate::data_values::FieldPathKey { + path: vec![], + field: f.origin, + }; + state.update(p, Event::ValueFocus(root.clone())).unwrap(); + state + .update(p, Event::ValueBrowse(Some(root.clone()))) + .unwrap(); + let inner = state + .draft + .as_ref() + .unwrap() + .browsed_values(p, exchange) + .unwrap() + .into_iter() + .find(|choice| choice.key.field == next) + .unwrap() + .key; + state.update(p, Event::ValueFocus(inner.clone())).unwrap(); + state + .update(p, Event::ValueBrowse(Some(inner.clone()))) + .unwrap(); + let leaf = state + .draft + .as_ref() + .unwrap() + .browsed_values(p, exchange) + .unwrap() + .into_iter() + .find(|choice| choice.key.field == f.leaf) + .unwrap() + .key; + assert_eq!(leaf.path, [f.origin, next]); + state.update(p, Event::ValueFocus(leaf.clone())).unwrap(); + state + .update(p, Event::Value(leaf.clone(), "keep this draft".into())) + .unwrap(); + state + .update(p, Event::ValueBrowse(crate::data_values::parent(&inner))) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().value_browse, Some(root)); + state.update(p, Event::ValueBrowse(None)).unwrap(); + assert_eq!( + state.draft.as_ref().unwrap().values[&leaf], + "keep this draft" + ); + assert_eq!( + panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1, + 1 + ); + let deepest = crate::data_values::FieldPathKey { + path: std::iter::once(f.origin) + .chain(std::iter::repeat_n(next, data::MAX_VALUE_PATH_DEPTH - 1)) + .collect(), + field: next, + }; + state.update(p, Event::ValueBrowse(Some(deepest))).unwrap(); + assert!( + state + .draft + .as_ref() + .unwrap() + .browsed_values(p, exchange) + .unwrap_err() + .contains("128") + ); + assert_eq!( + panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1, + 1 + ); + assert_eq!( + state.draft.as_ref().unwrap().values[&leaf], + "keep this draft" + ); + state + .update(p, Event::Value(leaf.clone(), String::new())) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + assert!(d.authored_values().is_empty()); + assert_eq!(d.values[&leaf], "", "empty input remains an editable draft"); + } + + #[test] + fn reply_call_picker_previews_the_same_value_reset_as_selecting_another_exchange() { + let mut f = crate::data_values::tests::payload(); + let p = &mut f.project; + let source = p.relations[&f.exchange].source(); + let target = p.relations[&f.exchange].target(); + let mut lifelines = vec![]; + for (name, represented) in [("Caller", source), ("Callee", target)] { + let id = p + .add_element(Kind::Lifeline, Layer::Oa, name, Some(f.scenario)) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(represented), + }; + lifelines.push(id); + } + let other_item = p + .add_element(Kind::ExchangeItem, Layer::Oa, "Other operation", None) + .unwrap(); + *p.elements + .get_mut(&other_item) + .unwrap() + .shared_mut() + .unwrap() = p.elements[&f.item].shared().unwrap().clone(); + let other_field = p + .add_element(Kind::DataField, Layer::Oa, "Result", Some(other_item)) + .unwrap(); + *p.elements + .get_mut(&other_field) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::DataField { + type_ref: TypeRef::Real, + direction: ParameterDirection::Out, + minimum: 1, + maximum: Some(1), + unit: String::new(), + storage: FieldStorage::Value, + default: None, + }; + let other_exchange = p.relate(RelationKind::Interaction, "Other exchange", source, target); + p.relations.get_mut(&other_exchange).unwrap().body = RelationBody::Interaction { + source, + target, + items: vec![other_item], + }; + let mut call_ids = vec![]; + for (name, exchange) in [("First call", f.exchange), ("Other call", other_exchange)] { + let call = occurrence( + p, + f.scenario, + name, + OccurrenceBody::Exchange { + operation: None, + exchange: Some(exchange), + sender: Some(lifelines[0]), + receiver: Some(lifelines[1]), + message_kind: MessageKind::Call, + reply_to: None, + values: vec![], + }, + ); + call_ids.push(call.id); + *p = execute_batch(p, &[Command::CreateElement(call)]).unwrap(); + } + let mut state = State::default(); + state + .update(p, Event::Start(f.scenario, Tool::Message)) + .unwrap(); + state.update(p, Event::Style(MessageStyle::Reply)).unwrap(); + state + .update(p, Event::Call(named_choice(p, call_ids[0]))) + .unwrap(); + let key = crate::data_values::FieldPathKey { + path: vec![f.destination], + field: f.leaf, + }; + state.update(p, Event::ValueFocus(key.clone())).unwrap(); + state + .update(p, Event::Value(key.clone(), "reply expression".into())) + .unwrap(); + state + .update(p, Event::ValueBrowse(crate::data_values::parent(&key))) + .unwrap(); + let offered = calls(p, state.draft.as_ref().unwrap()); + assert!( + offered.iter().any(|choice| choice.id == call_ids[1]), + "a different exchange remains selectable after entering a value" + ); + assert_eq!( + state.draft.as_ref().unwrap().values[&key], + "reply expression", + "preview is read-only" + ); + state + .update(p, Event::Call(named_choice(p, call_ids[0]))) + .unwrap(); + assert_eq!( + state.draft.as_ref().unwrap().values[&key], + "reply expression" + ); + state + .update(p, Event::Call(named_choice(p, call_ids[1]))) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + assert!(d.values.is_empty()); + assert!(d.value_focus.is_none()); + assert!(d.value_browse.is_none()); + assert_eq!(d.exchange, Some(other_exchange)); + assert!(state.prepare_pending(p).is_ok()); + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace.rs new file mode 100644 index 00000000..089ea4f3 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace.rs @@ -0,0 +1,697 @@ +//! Scenario presentation. Layout/camera controls never construct model commands. +use super::super::{form_session, scenario_scroll}; +use super::*; +use iced::widget::{Space, column, sensor}; + +pub(crate) const NAME_INPUT: &str = "scenario-form-name"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Shelf { + Tools, + Selection, +} + +#[derive(Clone, Default)] +pub(super) struct Presentation { + pub(super) identity: Option<(Id, Id)>, + pub(super) viewport: Option, + pub(super) focused: bool, + pub(super) shelf: Option, + pub(super) pan: iced::Vector, + pub(super) zoom: Option, + pub(super) fit: bool, +} +const MIN_SCALE: f32 = 0.05; +impl Presentation { + pub(super) fn viewport(&mut self, size: Size) -> bool { + if !size.width.is_finite() + || !size.height.is_finite() + || size.width <= 0. + || size.height <= 0. + || self.viewport == Some(size) + { + return false; + } + self.viewport = Some(size); + true + } + pub(super) fn zoom(&mut self, scale: Option) -> bool { + if scale.is_some_and(|value| !value.is_finite() || value <= 0.) { + return false; + } + let next = scale.map(|value| value.clamp(MIN_SCALE, 3.)); + let changed = self.zoom != next || self.fit != scale.is_none(); + self.zoom = next; + self.fit = scale.is_none(); + changed + } + fn fit_scale(&self, scene: Size) -> Option { + let viewport = self.viewport?; + // Fit uses the actual mounted viewport with a small visual gutter. + Some( + ((viewport.width - 16.).max(1.) / scene.width.max(1.)) + .min((viewport.height - 16.).max(1.) / scene.height.max(1.)), + ) + } + pub(super) fn scale(&self, scene: Size) -> f32 { + if self.fit { + self.fit_scale(scene).unwrap_or(1.).clamp(MIN_SCALE, 1.) + } else { + self.zoom.unwrap_or(1.) + } + } +} + +pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a, Message> { + let p = app.project(); + let state = &app.behavior.scenario; + let presentation = &state.presentation; + let pending = state + .draft + .as_ref() + .is_some_and(|draft| draft.owner == owner); + let context = app.scenario_context().expect("visible scenario context"); + let tool = state.canvas_tool(owner); + let clean = !app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty(); + let mut scene = Scene::sequence(p, owner, app.selected); + let scale = presentation.scale(scene.size); + scene.scale = scale; + scene.camera_pan = presentation.pan; + scene.interaction = Some((context, tool, clean)); + scene.pending_placement = state.pending_placement(owner); + let scope = super::super::canvas_workspace::Scope { + document: context.document, + owner, + revision: context.revision, + epoch: app.camera_epoch, + }; + let keyboard = clean && super::super::canvas_workspace::keyboard_enabled(app); + let camera = move |e| Message::ScenarioCanvas(CanvasEvent::Camera(context, scope.epoch, e)); + let canvas = container( + sensor( + super::super::canvas_workspace::Viewport::new( + canvas::Canvas::new(scene).width(Fill).height(Fill), + scope, + scale, + keyboard, + camera, + ) + .pan_background(tool == CanvasTool::Select), + ) + .key((context.document, owner)) + .on_show(move |size| Message::ScenarioCanvas(CanvasEvent::Viewport(context, size))) + .on_resize(move |size| Message::ScenarioCanvas(CanvasEvent::Viewport(context, size))), + ) + .id("scenario-viewport") + .width(Fill) + .height(Fill) + .style(style::card) + .into(); + let mut navigation = row![ + out.width(Fill), + super::super::canvas_workspace::Control::new( + button( + text(if presentation.focused { + "Restore panels" + } else { + "Focus canvas" + }) + .size(12) + ) + .padding(6) + .on_press(super::super::canvas_workspace::intent( + scope, + "Panels", + Message::ScenarioCanvas(CanvasEvent::Focus(context)) + )), + scope, + "Panels", + keyboard, + Some(super::super::canvas_workspace::intent( + scope, + "Panels", + Message::ScenarioCanvas(CanvasEvent::Focus(context)) + )), + camera + ) + ] + .spacing(6); + if state.can_back(p, context) { + navigation = + navigation.push(button(text("Back").size(12)).padding(6).on_press_maybe( + clean.then_some(Message::ScenarioCanvas(CanvasEvent::Back(context))), + )); + } + let header = column![ + navigation, + super::super::canvas_workspace::controls( + scope, + keyboard, + scale, + Message::ScenarioCanvas(CanvasEvent::Shelf(context, Shelf::Selection)), + clean.then_some(Message::ScenarioCanvas(CanvasEvent::Camera( + context, + scope.epoch, + super::super::canvas_workspace::Event::Focus + ))), + move |zoom| Message::ScenarioCanvas(CanvasEvent::Zoom(context, zoom)), + camera + ) + ] + .spacing(4); + let entries = [ + ("S", "Select / reorder occurrences", CanvasTool::Select), + ( + "L", + "Lifeline: click to choose represented work or participant", + CanvasTool::Add(Tool::Lifeline), + ), + ( + "C/R", + "Call and correlated reply", + CanvasTool::Message(MessageStyle::CallReply), + ), + ( + "M", + "Asynchronous message", + CanvasTool::Message(MessageStyle::Asynchronous), + ), + ( + "C", + "Synchronous call", + CanvasTool::Message(MessageStyle::Call), + ), + ( + "R", + "Reply to existing call", + CanvasTool::Message(MessageStyle::Reply), + ), + ( + "+L", + "Create participant", + CanvasTool::Message(MessageStyle::Create), + ), + ( + "-L", + "Delete participant", + CanvasTool::Message(MessageStyle::Delete), + ), + ("T", "Local timer", CanvasTool::Message(MessageStyle::Timer)), + ( + "A", + "Action: click a sequence position", + CanvasTool::Add(Tool::Action), + ), + ( + "St", + "State observation: click a sequence position", + CanvasTool::Add(Tool::Observation), + ), + ( + "ref", + "Reuse scenario: click a sequence position", + CanvasTool::Add(Tool::Reuse), + ), + ( + "alt", + "Fragment: click a sequence position", + CanvasTool::Add(Tool::Fragment), + ), + ]; + let palette = super::super::canvas_workspace::palette( + scope, + keyboard, + camera, + entries.into_iter().map(|(glyph, label, choice)| { + ( + glyph, + label, + choice == tool, + clean.then_some(Message::ScenarioCanvas(CanvasEvent::Choose( + context, choice, + ))), + ) + }), + ); + let mut details = column![ + text(format!("Selected: {}", p.named(app.selected))).size(12), + button(text("Properties").size(12)) + .on_press_maybe(clean.then_some(Message::EditProperties(app.selected))) + ] + .spacing(6); + if referenced_scenario(p, owner, app.selected).is_some() { + details = details.push(button(text("Open definition").size(12)).on_press_maybe( + clean.then_some(Message::ScenarioCanvas(CanvasEvent::OpenUse( + context, + app.selected, + ))), + )); + } + let details = details_pane( + context, + state.scroll_restore(context, ScrollArea::Page), + sequence_rows(p, context, owner, details, clean).into(), + ); + let mut form_panel = editor_panel( + Id::nil(), + Space::new().height(0).into(), + Space::new().height(0).into(), + ); + if let Some(d) = app + .behavior + .scenario + .draft + .as_ref() + .filter(|d| d.owner == owner) + { + if let Some(child) = d.source.view(p) { + form_panel = column![ + text(format!( + "Consumer name: {}", + crate::views::short(&d.name, 60) + )) + .size(12), + child.map(|e| event(Event::Source(e))) + ] + .spacing(6) + .height(Fill) + .into(); + } else { + let mut form = column![ + text(d.tool.label()).size(16), + text_input("Name (defaults to selected source)", &d.name) + .id(focus::name_id( + state.focus_stamp(context).expect("visible parent form") + )) + .on_input(|s| event(Event::Name(s))) + .padding(9) + .size(12) + ] + .spacing(9); + if d.tool != Tool::Lifeline { + form = form.push(label(format!( + "Insert: {}", + slot_label( + p, + Slot { + parent: d.parent, + before: d.before + } + ) + ))); + if d.before.is_some() { + form = form.push( + button("Append to this sequence instead") + .on_press(event(Event::AppendInstead)), + ); + } + form = form.push(picker( + parents(p, owner), + Some(d.parent), + "Destination sequence / operand…", + Event::Parent, + )); + } + if matches!( + d.tool, + Tool::Lifeline | Tool::Reuse | Tool::Action | Tool::Observation + ) { + form = form.push(picker( + sources(p, d), + d.target, + "Choose a named source…", + Event::Target, + )); + } + if d.tool == Tool::Lifeline { + if let Some(title) = d.source.label(d.target) { + form = form.push(label(title)).push(label( + "Create commits one new source and this lifeline together.", + )); + } + if viewpoint(p, owner) != ScenarioViewpoint::MixedLegacy { + form = form.push(button("Create new source…").on_press(event(Event::Source( + super::super::source_draft::Event::Open(d.invocation), + )))); + } else { + form=form.push(label("New source creation requires a strict viewpoint. Existing sources remain available.")); + } + } + if d.tool == Tool::Message { + form = form.push( + pick_list( + vec![ + MessageStyle::Asynchronous, + MessageStyle::Call, + MessageStyle::CallReply, + MessageStyle::Reply, + MessageStyle::Create, + MessageStyle::Delete, + MessageStyle::Timer, + ], + Some(d.style), + |style| event(Event::Style(style)), + ) + .width(Fill) + .padding(9) + .text_size(12), + ); + if d.style == MessageStyle::Reply { + form = form.push(bounded_picker( + app.behavior.scenario.call_choices(p, context, d), + d, + d.call, + "Choose the earlier call…", + Event::Call, + )); + if d.call.is_some() { + form = form.push(label( + "Reply reuses the call exchange in reverse and retains its correlation.", + )); + } + } else if d.style != MessageStyle::Timer { + form = form.push(bounded_picker( + exchanges(p, d), + d, + d.exchange, + "Choose the exchange…", + Event::Exchange, + )); + form = form.push(label( + "Apply checks that the message is valid at this insertion position.", + )); + } + form = form.push(picker( + d.line_choices(p, true), + d.sender, + "Sending lifeline…", + Event::Sender, + )); + if d.style != MessageStyle::Timer { + form = form.push(picker( + d.line_choices(p, false), + d.receiver, + "Receiving lifeline…", + Event::Receiver, + )); + } + if d.style == MessageStyle::Timer { + form = form.push( + text_input("Duration / deadline, e.g. 30 s", &d.expression) + .on_input(|s| event(Event::Expression(s))) + .padding(9) + .size(12), + ); + } + if p.layer_of(owner) == Some(Layer::Sa) + && viewpoint(p, owner) == ScenarioViewpoint::BoundaryInterface + && matches!( + d.style, + MessageStyle::Call | MessageStyle::CallReply | MessageStyle::Asynchronous + ) + { + form = form.push( + iced::widget::checkbox(d.operation.is_some()) + .label("Invoke a specific interface operation") + .on_toggle(|enabled| event(Event::OperationIntent(enabled))), + ); + if let Some(operation) = &d.operation { + form = form + .push(bounded_picker( + crate::data_values::operation_choices(p, &d.proposed_message(p)), + d, + operation.allocation, + "Choose the exact interface operation allocation…", + Event::Operation, + )) + .push( + button("Keep operation unresolved") + .on_press(event(Event::ClearOperation)), + ); + if let Some(id) = operation.allocation { + form = form.push(label(crate::data_values::operation_identity(p, id))); + } + } + } + if let Some(description) = + crate::data_values::operation_label(p, &d.proposed_message(p)) + { + form = form.push(label(description)); + } + match d.signature(p) { + Ok(Some(exchange)) => form = form.push(value_panel(p, d, &exchange)), + Err(problem) => { + form = form.push(label(problem)); + for (key, expression) in d.authored_values() { + form = form + .push( + text(format!( + "Retained value · {} / {}", + key.path + .iter() + .map(|id| p.named(*id)) + .collect::>() + .join(" → "), + p.named(key.field) + )) + .size(11), + ) + .push( + text_input("Value expression", expression) + .on_input({ + let key = key.clone(); + move |raw| event(Event::Value(key.clone(), raw)) + }) + .padding(8) + .size(12), + ); + } + } + Ok(None) => {} + } + if d.style == MessageStyle::CallReply { + form=form.push(label("Creates the call and reversed, correlated reply together in one undoable operation.")); + } + } + if matches!(d.tool, Tool::Action | Tool::Observation) { + form = form.push(picker( + lines(p, owner), + d.sender, + "Observed lifeline…", + Event::Sender, + )); + } + if d.tool == Tool::Reuse + && let Some(target) = d.target + { + for line in lines(p, target) { + let referenced = line.id; + let compatible: Vec<_> = lines(p, owner) + .into_iter() + .filter(|c| { + compatible_binding(p, referenced, c.id) + && !d + .bindings + .iter() + .any(|(r, l)| *r != referenced && *l == c.id) + }) + .collect(); + if compatible.is_empty() { + form = form.push(label(format!( + "Add a compatible local lifeline before binding '{}'.", + p.named(referenced) + ))); + } + form = form + .push(label(format!("Bind {}", p.named(referenced)))) + .push(picker( + compatible, + d.bindings.get(&referenced).copied(), + "Compatible local lifeline…", + move |c| Event::Binding(referenced, c), + )); + } + form = form.push(label( + "Bindings are explicit, one-to-one and refer to existing lifelines.", + )); + } + if d.tool == Tool::Fragment { + form = form.push( + pick_list( + vec![ + Operator(FragmentOperator::Alternative), + Operator(FragmentOperator::Parallel), + Operator(FragmentOperator::Loop), + Operator(FragmentOperator::Optional), + ], + Some(Operator(d.operator)), + |o| event(Event::Operator(o)), + ) + .padding(9) + .text_size(12) + .width(Fill), + ); + if d.operator != FragmentOperator::Parallel { + form = form.push( + text_input("Condition / loop guard", &d.expression) + .on_input(|s| event(Event::Expression(s))) + .padding(9) + .size(12), + ); + } + if d.operator == FragmentOperator::Alternative { + form = form.push( + text_input("Second alternative guard", &d.second_guard) + .on_input(|s| event(Event::SecondGuard(s))) + .padding(9) + .size(12), + ); + } + form=form.push(label("Creates the fragment with its operands. Choose an operand as destination when adding messages or actions.")); + } + let readiness = d.command(p); + if let Err(error) = &readiness { + form = form.push(label(error.clone())); + } + let footer = row![ + button("Create") + .padding(9) + .on_press_maybe(readiness.is_ok().then_some(event(Event::Submit))), + action("Cancel", event(Event::Cancel)) + ] + .spacing(7); + let session = state.form_session().expect("visible scenario draft"); + let mount = state.form_mount().expect("visible parent form"); + form_panel = sensor(editor_panel(session, footer.into(), form.into())) + .key(mount) + .on_show(move |_| Message::ScenarioCanvas(CanvasEvent::FormShown(context, mount))) + .into(); + } + } + let editor = if pending { + Some(form_panel) + } else { + presentation.shelf.map(|_| details) + }; + super::super::canvas_workspace::host(header.into(), palette, canvas, editor) +} + +fn sequence_action<'a>( + title: &str, + message: Message, + enabled: bool, +) -> iced::widget::Button<'a, Message> { + button(text(title.to_owned()).size(12)) + .padding(8) + .style(style::nav) + .on_press_maybe(enabled.then_some(message)) +} + +fn details_pane<'a, R: iced::advanced::text::Renderer + 'a>( + context: Context, + restore: Option, + details: iced::Element<'a, Message, Theme, R>, +) -> iced::Element<'a, Message, Theme, R> { + scenario_scroll::observe( + context, + ScrollArea::Page, + false, + restore, + scrollable(details) + .id("scenario-details") + .width(Fill) + .height(Fill), + ) +} + +fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>( + session: Id, + actions: iced::Element<'a, Message, Theme, R>, + fields: iced::Element<'a, Message, Theme, R>, +) -> iced::Element<'a, Message, Theme, R> { + container( + column![ + container(actions).id("scenario-actions").width(Fill), + form_session::reset_on_change( + session, + scrollable(fields) + .id("scenario-form") + .width(Fill) + .height(Fill), + ), + ] + .spacing(6), + ) + .padding(10) + .width(Fill) + .height(Fill) + .style(style::card) + .into() +} + +#[cfg(all(test, debug_assertions))] +mod tests; + +fn sequence_rows<'a>( + p: &'a Project, + context: Context, + owner: Id, + mut out: Column<'a, Message>, + enabled: bool, +) -> Column<'a, Message> { + if let Some(SharedBody::Occurrence { + body: OccurrenceBody::Fragment { branches, .. }, + .. + }) = p.elements.get(&owner).and_then(|e| e.shared()) + { + for branch in branches { + out = out.push(sequence_action( + &format!("Operand: {}", p.named(*branch)), + Message::Select(*branch), + enabled, + )); + out = sequence_rows(p, context, *branch, out, enabled); + } + } + if let Some(steps) = p.elements.get(&owner).and_then(|e| e.steps()) { + for (i, id) in steps.iter().enumerate() { + out = out.push( + row![ + sequence_action( + &format!("{}. {}", i + 1, p.named(*id)), + Message::Select(*id), + enabled + ) + .width(Fill), + button("↑") + .padding(8) + .on_press_maybe((enabled && i > 0).then_some(Message::Command( + Command::MoveStep(owner, i, i.saturating_sub(1)) + ))), + button("↓").padding(8).on_press_maybe( + (enabled && i + 1 < steps.len()) + .then_some(Message::Command(Command::MoveStep(owner, i, i + 1))) + ) + ] + .spacing(6), + ); + if let Some(SharedBody::Occurrence { + body: + OccurrenceBody::ScenarioUse { + scenario: Some(target), + .. + }, + .. + }) = p.elements[id].shared() + { + out = out.push(sequence_action( + &format!("Open {} →", p.named(*target)), + Message::ScenarioCanvas(CanvasEvent::OpenUse(context, *id)), + enabled, + )); + } + out = sequence_rows(p, context, *id, out, enabled); + } + } + out +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs new file mode 100644 index 00000000..0aa066dd --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/scenario/workspace/tests.rs @@ -0,0 +1,1955 @@ +use super::*; +use crate::behavior::canvas_workspace::tests::{Geometry, Mounted}; +use iced::Vector; +use iced::advanced::{Layout, layout, widget}; + +#[tokio::test] +async fn production_scenario_canvas_keeps_bounds_draft_identity_and_camera_through_editing() { + use crate::behavior::canvas_workspace::{self, tests::Mounted}; + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + for size in [Size::new(1100., 700.), Size::new(1440., 900.)] { + app.window_size = size; + mounted.settle(&mut app).await; + for scale in [0.5, 1., 2.] { + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Zoom( + app.scenario_context().unwrap(), + Some(scale), + ))); + let geometry = mounted.geometry(&app); + let bounds = geometry.bounds("scenario-viewport"); + assert!( + !geometry + .scrolls + .contains_key(&widget::Id::new("scenario-canvas")) + ); + assert!( + bounds.height > 350., + "actual Fill viewport at {size:?}: {bounds:?}" + ); + let scene = Scene::sequence(app.project(), f.owner, f.owner); + let slot = scene + .slots + .iter() + .find(|s| s.slot.parent == f.owner && s.slot.before == Some(f.reply)) + .unwrap(); + let expected = slot.slot; + let world = Point::new(scene.lanes[&f.sender], slot.y); + let desired = Vector::new(70., 115.); + let next_pan = desired - Vector::new(world.x, world.y) * scale; + let delta = next_pan - app.behavior.scenario.presentation.pan; + let start = bounds.position() + Vector::new(120., 60.); + let snapshot = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + mounted.pan(&mut app, start, start + delta).await; + let diff = app.behavior.scenario.presentation.pan - next_pan; + assert!(diff.x.hypot(diff.y) < 0.01); + let tool = mounted.geometry(&app).bounds("behavior-tool:A").center(); + mounted.click(&mut app, tool).await; + let messages = mounted.click(&mut app, bounds.position() + desired).await; + assert!( + messages.iter().any( + |m| matches!(m, Message::ScenarioCanvas(CanvasEvent::BeginOccurrence { + tool: Tool::Action, slot, lifeline: Some(line), .. + }) if *slot == expected && *line == f.sender) + ), + "scale {scale}: {messages:?}" + ); + let session = app.behavior.scenario.form_session().unwrap(); + let invocation = app.behavior.scenario.draft.as_ref().unwrap().invocation; + let _ = app.update(event(Event::Name(" raw action 雨 ".into()))); + let raw = app.behavior.recovery_snapshot().unwrap(); + mounted.settle(&mut app).await; + let shown = mounted.geometry(&app); + assert_eq!( + app.behavior.scenario.pending_placement(f.owner), + Some((expected, Some(f.sender))) + ); + let retained_pan = app.behavior.scenario.presentation.pan - next_pan; + assert!( + retained_pan.x.hypot(retained_pan.y) < 0.01, + "opening the editor must not move the clicked, already-visible insertion marker" + ); + assert_eq!(shown.bounds("scenario-viewport"), bounds); + let actions = shown.bounds("scenario-actions"); + let form = shown.scrolls[&widget::Id::new("scenario-form")].0; + assert!(actions.y >= bounds.y && actions.y + actions.height <= form.y); + assert!(form.y + form.height <= bounds.y + bounds.height); + assert!(form.height > 250.); + let pan = app.behavior.scenario.presentation.pan; + // Background canvas keys cannot move the camera while the draft owns editing. + mounted + .batch( + &mut app, + &[canvas_workspace::tests::key( + iced::keyboard::key::Named::ArrowDown, + )], + mouse::Cursor::Available(bounds.position() + desired), + ) + .await; + assert_eq!(app.behavior.scenario.presentation.pan, pan); + mounted.scroll(&app, "scenario-form", 500.); + let offset = mounted.geometry(&app).scrolls[&widget::Id::new("scenario-form")].1; + assert_eq!(mounted.geometry(&app).bounds("scenario-actions"), actions); + for shelf in [Shelf::Tools, Shelf::Selection, Shelf::Selection] { + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Shelf( + app.scenario_context().unwrap(), + shelf, + ))); + assert_eq!(mounted.geometry(&app).bounds("scenario-viewport"), bounds); + assert_eq!(app.behavior.scenario.form_session(), Some(session)); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().invocation, + invocation + ); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert_eq!( + mounted.geometry(&app).scrolls[&widget::Id::new("scenario-form")].1, + offset + ); + } + // Width-only responsive changes preserve the actual editor offset, + // identity and camera; the right editor keeps its fixed field width. + let other_width = if size.width == 1100. { 1440. } else { 1100. }; + for resized in [Size::new(other_width, size.height), size] { + let task = app.update(Message::WindowResized(resized)); + mounted.perform(&mut app, task).await; + mounted.settle(&mut app).await; + let resized_geometry = mounted.geometry(&app); + assert_eq!( + resized_geometry.scrolls[&widget::Id::new("scenario-form")].1, + offset + ); + assert_eq!(resized_geometry.bounds("scenario-actions").y, actions.y); + assert_eq!(app.behavior.scenario.form_session(), Some(session)); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().invocation, + invocation + ); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert_eq!(app.behavior.scenario.presentation.pan, pan); + } + let _ = app.update(event(Event::Cancel)); + assert_eq!(mounted.geometry(&app).bounds("scenario-viewport"), bounds); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo() && !app.document.can_redo()); + // Close the optional details overlay before the next pointer workflow. + app.behavior.scenario.presentation.shelf = None; + } + } +} + +#[tokio::test] +async fn production_scenario_canvas_pan_invalidates_a_pressed_creation_and_caller_restores_once() { + use super::super::direct_tests::{navigation_event, navigation_fixture}; + use crate::behavior::canvas_workspace::{Event as Camera, tests::Mounted}; + let (p, [owner, target, _], [reference, _, _]) = navigation_fixture(); + let mut app = App::with_project(p); + let _ = app.update(Message::Select(owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let bounds = mounted.geometry(&app).bounds("scenario-viewport"); + let source = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + navigation_event(&mut app, |c| { + CanvasEvent::Choose(c, CanvasTool::Add(Tool::Fragment)) + }); + let point = bounds.position() + Vector::new(80., 120.); + mounted + .batch( + &mut app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + ], + mouse::Cursor::Available(point), + ) + .await; + assert!( + !app.behavior.has_pending(), + "a zero-distance camera press cancels semantic creation" + ); + let emitted = mounted + .pan(&mut app, point, point + Vector::new(-45., 28.)) + .await; + let retained = crate::behavior::canvas_workspace::tests::camera_delta(&emitted); + let camera = ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom, + ); + let toolbar = + crate::behavior::canvas_workspace::tests::capture_toolbar(&mut mounted, &app, "Fit"); + navigation_event(&mut app, |c| CanvasEvent::OpenUse(c, reference)); + mounted.settle(&mut app).await; + assert_eq!(app.scenario_context().unwrap().scenario, target); + let lifetime = app.camera_epoch; + navigation_event(&mut app, |c| { + CanvasEvent::Camera(c, lifetime, Camera::Pan(Vector::new(90., -70.))) + }); + navigation_event(&mut app, CanvasEvent::Back); + mounted.settle(&mut app).await; + assert_eq!(app.selected, reference); + assert_eq!(app.scenario_context().unwrap().scenario, owner); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera + ); + let task = app.update(retained); + mounted.perform(&mut app, task).await; + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera, + "a camera packet retained before A → B → Back must not move the restored caller" + ); + let lifetime = app.camera_epoch; + let task = app.update(toolbar); + mounted.perform(&mut app, task).await; + assert_eq!( + app.camera_epoch, lifetime, + "A to B and Back cannot revive toolbar intent" + ); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera + ); + let lifetime = app.camera_epoch; + navigation_event(&mut app, |c| { + CanvasEvent::Camera(c, lifetime, Camera::Pan(Vector::new(10., 15.))) + }); + mounted.settle(&mut app).await; + assert_eq!( + app.behavior.scenario.presentation.pan, + camera.0 + Vector::new(10., 15.) + ); + assert!(std::sync::Arc::ptr_eq(&source, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_scenario_whole_glyph_drag_moves_one_occurrence_and_rejects_other_operand() { + use crate::behavior::canvas_workspace::tests::Mounted; + let (mut f, branch, first, _, _, _) = super::super::direct_tests::placement_fixture(); + let other = super::super::super::topology::occurrence( + &f.project, + branch, + "Second check", + OccurrenceBody::Action { + action: represented(&f.project, f.sender), + lifeline: Some(f.sender), + }, + ); + let second = other.id; + f.project = execute_batch(&f.project, &[Command::CreateElement(other)]).unwrap(); + let mut app = App::with_project(f.project.clone()); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let bounds = mounted.geometry(&app).bounds("scenario-viewport"); + let scene = Scene::sequence(app.project(), f.owner, f.owner); + let glyph = scene + .boxes + .iter() + .find(|b| b.id == first && !b.title.is_empty()) + .unwrap() + .rect; + let end = scene + .slots + .iter() + .find(|s| s.slot.parent == branch && s.slot.before.is_none()) + .unwrap(); + let forbidden = scene + .slots + .iter() + .find(|s| s.slot.parent == f.owner) + .unwrap(); + let world_start = glyph.position() + Vector::new(25., 12.); + let scale = 0.5; + let pan = Vector::new(70. - world_start.x * scale, 260. - world_start.y * scale); + let screen = |point: Point| bounds.position() + pan + Vector::new(point.x, point.y) * scale; + let start = screen(world_start); + let valid = screen(Point::new(world_start.x, end.y)); + let invalid = screen(Point::new(world_start.x, forbidden.y)); + assert!( + bounds.contains(start) && bounds.contains(valid) && bounds.contains(invalid), + "all drag destinations must be visible" + ); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Zoom( + app.scenario_context().unwrap(), + Some(scale), + ))); + let delta = pan - app.behavior.scenario.presentation.pan; + let context = app.scenario_context().unwrap(); + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Camera( + context, + app.camera_epoch, + crate::behavior::canvas_workspace::Event::Pan(delta), + ))); + let snapshot = app.document.snapshot(); + let before = app.document.bundle().clone(); + let drag = |end| { + [ + iced::Event::Mouse(mouse::Event::CursorMoved { position: start }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + iced::Event::Mouse(mouse::Event::CursorMoved { position: end }), + iced::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + ] + }; + let messages = mounted + .batch(&mut app, &drag(invalid), mouse::Cursor::Available(invalid)) + .await; + assert!( + !messages + .iter() + .any(|m| matches!(m, Message::ScenarioCanvas(CanvasEvent::Reorder { .. }))) + ); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + let messages = mounted + .batch(&mut app, &drag(valid), mouse::Cursor::Available(valid)) + .await; + assert!(messages.iter().any( + |m| matches!(m, Message::ScenarioCanvas(CanvasEvent::Reorder { occurrence, slot, .. }) + if *occurrence == first && *slot == (Slot { parent: branch, before: None })) + )); + assert_eq!(app.selected, first); + let expected = execute_batch( + &f.project, + &[Command::SetSteps(branch, vec![second, first])], + ) + .unwrap(); + assert_eq!(app.project(), &expected); + let after = app.document.bundle().clone(); + let _ = app.update(Message::Undo); + assert_eq!(app.project(), &f.project); + assert_eq!(app.document.bundle(), &before); + assert!(!app.document.can_undo()); + let _ = app.update(Message::Redo); + assert_eq!(app.project(), &expected); + assert_eq!(app.document.bundle(), &after); + assert!(!app.document.can_redo()); +} + +#[tokio::test] +async fn production_empty_scenario_lifeline_tool_keeps_staged_source_inside_stable_canvas() { + use crate::behavior::canvas_workspace::tests::Mounted; + let mut project = Project::empty(); + let owners = [Layer::Oa, Layer::Sa].map(|layer| { + project + .add_element(Kind::Scenario, layer, "Empty scenario", None) + .unwrap() + }); + let mut app = App::with_project(project.clone()); + let _ = app.update(Message::Select(owners[0])); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + for owner in owners { + let _ = app.update(Message::Select(owner)); + mounted.settle(&mut app).await; + let bounds = mounted.geometry(&app).bounds("scenario-viewport"); + let tool = mounted.geometry(&app).bounds("behavior-tool:L").center(); + mounted.click(&mut app, tool).await; + mounted + .click(&mut app, bounds.position() + Vector::new(70., 100.)) + .await; + let draft = app + .behavior + .scenario + .draft + .as_ref() + .expect("canvas lifeline opens its single typed draft"); + assert_eq!(draft.owner, owner); + assert_eq!(draft.tool, Tool::Lifeline); + let invocation = draft.invocation; + let _ = app.update(event(Event::Name(" consumer raw 雨 ".into()))); + let snapshot = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + let _ = app.update(event(Event::Source( + crate::behavior::source_draft::Event::Open(invocation), + ))); + assert!(app.behavior.scenario.source_open()); + mounted.settle(&mut app).await; + assert_eq!(mounted.geometry(&app).bounds("scenario-viewport"), bounds); + let _ = app.update(Message::Escape); + assert!(!app.behavior.scenario.source_open()); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().invocation, + invocation + ); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().name, + " consumer raw 雨 " + ); + mounted.settle(&mut app).await; + assert_eq!(mounted.geometry(&app).bounds("scenario-viewport"), bounds); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + let _ = app.update(event(Event::Cancel)); + assert!(!app.document.can_undo() && !app.document.can_redo()); + } +} + +#[tokio::test] +async fn scenario_form_focus_targets_the_name_and_rejects_stale_sessions_or_modals() { + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let _ = app.update(event(Event::Start(f.owner, Tool::Fragment))); + let _ = app.update(event(Event::Name(" retained 雨 ".into()))); + let context = app.scenario_context().unwrap(); + let session = app.behavior.scenario.form_session().unwrap(); + let mount = app.behavior.scenario.form_mount().unwrap(); + app.window_size = Size::new(1100., 520.); + let raw = app.behavior.recovery_snapshot().unwrap(); + let accepted = app.document.snapshot(); + for mismatch in 0..4 { + let mut stale = context; + let mut token = mount; + match mismatch { + 0 => stale.document = Id::new_v4(), + 1 => stale.scenario = Id::new_v4(), + 2 => stale.revision += 1, + _ => token = Id::new_v4(), + } + let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + stale, token, + ))); + assert!(iced_runtime::task::into_stream(task).is_none()); + } + app.hosted.visible = true; + let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + context, mount, + ))); + assert!(iced_runtime::task::into_stream(task).is_none()); + app.hosted.visible = false; + app.busy = true; + let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + context, mount, + ))); + assert!(iced_runtime::task::into_stream(task).is_none()); + app.busy = false; + + // The viewport report from the same mount may advance its geometry epoch. + // That does not change the identity of the field awaiting its first focus. + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport( + context, + Size::new(480., 180.), + ))); + assert_ne!(app.scenario_context(), Some(context)); + let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + context, mount, + ))); + let mut mounted = Mounted::unshown().await; + focus_other(&app, &mut mounted); + let mut away = widget::operation::scrollable::scroll_to::<()>( + widget::Id::new("scenario-form"), + widget::operation::scrollable::AbsoluteOffset { + x: None, + y: Some(1000.), + }, + ); + mounted.operate(&app, &mut away); + let mut before_reveal = Geometry::default(); + mounted.operate(&app, &mut before_reveal); + assert!( + before_reveal.scrolls[&widget::Id::new("scenario-form")].1.y > 64., + "the production parent must actually overflow" + ); + mounted.perform(&mut app, task).await; + assert_parent_focused(&app, &mut mounted); + let mut revealed = ParentBounds::new(app.scenario_focus_owner().unwrap()); + mounted.operate(&app, &mut revealed); + let name = revealed.name.expect("mounted parent Name"); + let viewport = revealed.viewport.expect("actual parent scroll viewport"); + assert!( + name.y >= viewport.y - 0.5 && name.y + name.height <= viewport.y + viewport.height + 0.5, + "Name {name:?} must be visible inside {viewport:?}" + ); + let repeated = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + context, mount, + ))); + assert!(iced_runtime::task::into_stream(repeated).is_none()); + assert_eq!(app.behavior.scenario.form_session(), Some(session)); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert!(!app.document.can_undo()); + let _ = app.update(event(Event::Cancel)); + let _ = app.update(event(Event::Start(f.owner, Tool::Fragment))); + let task = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + app.scenario_context().unwrap(), + mount, + ))); + assert!(iced_runtime::task::into_stream(task).is_none()); +} +// These gates mount the production App, including its modal and scenario +// branches. A missing headless backend is a test failure, never a silent skip. +const FOCUS_VIEWPORT: Size = Size::new(1100., 700.); +async fn focus_action(task: iced::Task) -> Box { + use iced::futures::StreamExt; + let mut stream = iced_runtime::task::into_stream(task).expect("parent focus task"); + let Some(iced_runtime::Action::Widget(operation)) = stream.next().await else { + panic!("focus must emit only a widget operation"); + }; + // The operation owns the task channel until it is dropped; polling its + // completion while retaining this delayed action would wait forever. + operation +} +#[derive(Default, Debug, PartialEq)] +struct Focused(Vec<(Option, bool)>); +impl widget::Operation for Focused { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { + visit(self); + } + fn focusable( + &mut self, + id: Option<&widget::Id>, + _: Rectangle, + state: &mut dyn widget::operation::Focusable, + ) { + self.0.push((id.cloned(), state.is_focused())); + } +} +fn parent_fields(app: &App, mounted: &mut Mounted) -> Focused { + let mut result = Focused::default(); + mounted.operate(app, &mut result); + result +} +struct FocusIndex { + target: usize, + index: usize, +} +impl widget::Operation for FocusIndex { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { + visit(self); + } + fn focusable( + &mut self, + _: Option<&widget::Id>, + _: Rectangle, + state: &mut dyn widget::operation::Focusable, + ) { + if self.index == self.target { + state.focus(); + } else { + state.unfocus(); + } + self.index += 1; + } +} +fn focus_other(app: &App, mounted: &mut Mounted) { + let parent = app.scenario_focus_owner().map(focus::name_id); + let fields = parent_fields(app, mounted); + let target = fields + .0 + .iter() + .rposition(|(id, _)| parent.is_none() || *id != parent) + .expect("actual tree contains another focusable input"); + mounted.operate(app, &mut FocusIndex { target, index: 0 }); + assert!(parent_fields(app, mounted).0[target].1); +} +fn focus_source(app: &App, mounted: &mut Mounted) { + let id = widget::Id::new(crate::behavior::source_draft::NAME_INPUT); + assert!( + parent_fields(app, mounted) + .0 + .iter() + .any(|(field, _)| field.as_ref() == Some(&id)) + ); + let mut operation = widget::operation::focusable::focus::<()>(id.clone()); + mounted.operate(app, &mut operation); + assert!(parent_fields(app, mounted).0.contains(&(Some(id), true))); +} +#[derive(Default)] +struct ReviewField { + inside: bool, + entering: Option, + index: usize, + target: Option, +} +impl widget::Operation for ReviewField { + fn container(&mut self, id: Option<&widget::Id>, _: Rectangle) { + self.entering = + Some(self.inside || id == Some(&widget::Id::new("reference-owning-review-modal"))); + } + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { + let prior = self.inside; + if let Some(inside) = self.entering.take() { + self.inside = inside; + } + visit(self); + self.inside = prior; + } + fn focusable( + &mut self, + _: Option<&widget::Id>, + _: Rectangle, + _: &mut dyn widget::operation::Focusable, + ) { + if self.inside { + self.target = Some(self.index); + } + self.index += 1; + } +} +fn focus_review(app: &App, mounted: &mut Mounted) { + let mut field = ReviewField::default(); + mounted.operate(app, &mut field); + let target = field + .target + .expect("production Review owns a mounted candidate input"); + mounted.operate(app, &mut FocusIndex { target, index: 0 }); + assert!(parent_fields(app, mounted).0[target].1); +} +fn assert_parent_focused(app: &App, mounted: &mut Mounted) { + let expected = focus::name_id(app.scenario_focus_owner().unwrap()); + let fields = parent_fields(app, mounted); + assert_eq!( + fields + .0 + .iter() + .filter(|(_, focused)| *focused) + .collect::>(), + vec![&(Some(expected), true)] + ); +} +struct ParentBounds { + target: widget::Id, + translation: Vector, + pending: Option, + viewport: Option, + name: Option, +} +impl ParentBounds { + fn new(stamp: focus::Stamp) -> Self { + Self { + target: focus::name_id(stamp), + translation: Vector::ZERO, + pending: None, + viewport: None, + name: None, + } + } + fn actual(&self, bounds: Rectangle) -> Rectangle { + Rectangle { + x: bounds.x - self.translation.x, + y: bounds.y - self.translation.y, + ..bounds + } + } +} +impl widget::Operation for ParentBounds { + fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { + let previous = self.translation; + if let Some(offset) = self.pending.take() { + self.translation += offset; + } + visit(self); + self.translation = previous; + } + fn scrollable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _: Rectangle, + translation: Vector, + _: &mut dyn widget::operation::Scrollable, + ) { + if id == Some(&widget::Id::new("scenario-form")) { + self.viewport = Some(self.actual(bounds)); + } + self.pending = Some(translation); + } + fn focusable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _: &mut dyn widget::operation::Focusable, + ) { + if id == Some(&self.target) { + self.name = Some(self.actual(bounds)); + } + } +} +async fn shown_task(app: &mut App, mounted: &mut Mounted) -> iced::Task { + let messages = mounted.input( + app, + &[iced::Event::Window(iced::window::Event::RedrawRequested( + std::time::Instant::now(), + ))], + mouse::Cursor::Unavailable, + ); + let mut shown = None; + let mut ready = vec![]; + // Drain the whole App message batch before any operation sees the final UI. + // Retain only the parent sensor's Task for the explicit delayed-focus gates. + for message in messages { + let parent = matches!( + &message, + Message::ScenarioCanvas(CanvasEvent::FormShown(..)) + ); + let task = app.update(message); + if parent { + assert!(shown.replace(task).is_none()); + } else { + ready.push(task); + } + } + mounted.perform(app, iced::Task::batch(ready)).await; + shown.expect("production parent sensor reports its newly mounted identity") +} +fn focus_fixture() -> (App, Id, Id) { + let mut p = Project::empty(); + let owner = p + .add_element(Kind::Scenario, Layer::Oa, "Operational flow", None) + .unwrap(); + p.add_element(Kind::Scenario, Layer::Oa, "Another scenario", None) + .unwrap(); + if let Some(SharedBody::Scenario { viewpoint, .. }) = + p.elements.get_mut(&owner).unwrap().shared_mut() + { + *viewpoint = ScenarioViewpoint::OperationalActivity; + } + // A real Review candidate input is needed for the delayed-modal falsifier. + let oa = p + .add_element(Kind::DataType, Layer::Oa, "Operational number", None) + .unwrap(); + let sa = p + .add_element(Kind::DataType, Layer::Sa, "System number", None) + .unwrap(); + for (id, unit) in [(oa, "m"), (sa, "cm")] { + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType { + definition: DataTypeDefinition::Integer { + minimum: None, + maximum: None, + }, + default: None, + unit: unit.into(), + }; + } + let trace = Id::new_v4(); + p.traces.insert( + trace, + Trace { + id: trace, + alias: format!("review_{}", trace.simple()), + name: "Numbers review".into(), + description: String::new(), + oa, + sa, + kind: TraceKind::Manual, + baseline: None, + }, + ); + let mut app = App::with_project(p); + app.window_size = FOCUS_VIEWPORT; + let _ = app.update(Message::Select(owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let _ = app.update(event(Event::Start(owner, Tool::Lifeline))); + let _ = app.update(event(Event::Name(" retained parent 雨 ".into()))); + (app, owner, oa) +} +fn open_source(app: &mut App) -> Id { + use crate::behavior::source_draft as source; + let invocation = app.behavior.scenario.draft.as_ref().unwrap().invocation; + let _ = app.update(event(Event::Source(source::Event::Open(invocation)))); + assert!(app.behavior.scenario.source_open(), "{}", app.status); + let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap(); + serde_json::from_value(raw["source"]["editing"]["token"].clone()).unwrap() +} +fn open_review(app: &mut App, source: Id) { + use crate::semantic_review::{self, RowEvent}; + let mut review = semantic_review::State::new(app.project(), source).unwrap(); + let facet = FacetSelector::Data(DataFacet::Unit); + for event in [ + RowEvent::Select(facet.clone()), + RowEvent::Decide(facet, Some(SemanticDecision::Candidate)), + ] { + let addressed = review.address_event(event); + review.update(app.project(), addressed); + } + app.workflow = Some(crate::workflow::Workflow::Semantic(Box::new(review))); +} + +#[tokio::test] +async fn scenario_form_focus_returns_from_back_use_and_escape_after_initial_focus_is_consumed() { + use crate::behavior::source_draft as source; + for route in 0..3 { + let (mut app, _, _) = focus_fixture(); + let mut mounted = Mounted::unshown().await; + let initial = shown_task(&mut app, &mut mounted).await; + mounted.perform(&mut app, initial).await; + assert_parent_focused(&app, &mut mounted); + let first_mount = app.behavior.scenario.form_mount().unwrap(); + let session = app.behavior.scenario.form_session(); + let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap(); + let accepted = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + let token = open_source(&mut app); + focus_source(&app, &mut mounted); // mount the actual child + match route { + 0 => { + let _ = app.update(event(Event::Source(source::Event::Edit( + token, + source::Edit::Back, + )))); + } + 1 => { + let _ = app.update(event(Event::Source(source::Event::Edit( + token, + source::Edit::Name("New activity 雨".into()), + )))); + let _ = app.update(event(Event::Source(source::Event::Edit( + token, + source::Edit::Use, + )))); + } + _ => { + let _ = app.update(Message::Escape); + } + } + assert!(!app.behavior.scenario.source_open(), "{}", app.status); + assert_eq!(app.behavior.scenario.form_session(), session); + assert_ne!(app.behavior.scenario.form_mount(), Some(first_mount)); + let returned = app.behavior.scenario.recovery_snapshot().unwrap().unwrap(); + for key in [ + "name", + "invocation", + "owner", + "parent", + "before", + "source_origin", + ] { + assert_eq!(returned[key], raw[key], "route {route}, raw field {key}"); + } + let task = shown_task(&mut app, &mut mounted).await; + mounted.perform(&mut app, task).await; + assert_parent_focused(&app, &mut mounted); + assert_eq!( + app.behavior.scenario.recovery_snapshot().unwrap().unwrap(), + returned + ); + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo()); + assert!(!app.document.can_redo()); + let repeated = app.update(Message::ScenarioCanvas(CanvasEvent::FormShown( + app.scenario_context().unwrap(), + app.behavior.scenario.form_mount().unwrap(), + ))); + assert!(iced_runtime::task::into_stream(repeated).is_none()); + } +} + +#[tokio::test] +async fn scenario_form_focus_saved_action_is_inert_on_replacement_child_review_and_changed_context() +{ + for change in 0..8 { + let (mut app, owner, review_source) = focus_fixture(); + let mut mounted = Mounted::unshown().await; + let task = shown_task(&mut app, &mut mounted).await; + // Obtain S1's actual Action before changing the live state; stale + // FormShown rejection alone would not exercise this race. + let mut operation = focus_action(task).await; + match change { + 0 => { + let _ = app.update(event(Event::Cancel)); + let _ = app.update(event(Event::Start(owner, Tool::Lifeline))); + } + 1 => { + open_source(&mut app); + } + 2 => open_review(&mut app, review_source), + 3 => app.canvas_document = Id::new_v4(), + 4 => { + app.document + .execute(Command::Rename(owner, "Renamed scenario".into())) + .unwrap(); + } + 5 => { + let other = app + .project() + .elements + .values() + .find(|e| e.kind() == Kind::Scenario && e.id != owner) + .unwrap() + .id; + let draft = app.behavior.scenario.draft.as_mut().unwrap(); + draft.owner = other; + draft.parent = other; + } + 6 => { + open_source(&mut app); + focus_source(&app, &mut mounted); + let _ = app.update(Message::Escape); // same S1 session, different mount + } + _ => app.hosted.visible = true, + } + if change == 1 { + focus_source(&app, &mut mounted); + } else if change == 2 { + focus_review(&app, &mut mounted); + } else { + focus_other(&app, &mut mounted); + } + let before = parent_fields(&app, &mut mounted); + let source = app.document.bundle().clone(); + let raw = app.behavior.recovery_snapshot().unwrap(); + let undo = app.document.can_undo(); + let redo = app.document.can_redo(); + mounted.operate(&app, operation.as_mut()); + assert!( + matches!(operation.finish(), widget::operation::Outcome::None), + "change {change}" + ); + assert_eq!(parent_fields(&app, &mut mounted), before, "change {change}"); + assert_eq!(app.document.bundle(), &source); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert_eq!( + (app.document.can_undo(), app.document.can_redo()), + (undo, redo) + ); + } +} + +#[tokio::test] +async fn scenario_form_focus_chained_apply_rechecks_live_review_owner() { + let (mut app, _, review_source) = focus_fixture(); + let mut mounted = Mounted::unshown().await; + let task = shown_task(&mut app, &mut mounted).await; + let mut collect = focus_action(task).await; + mounted.operate(&app, collect.as_mut()); + let widget::operation::Outcome::Chain(mut apply) = collect.finish() else { + panic!("current parent produces the guarded apply traversal"); + }; + open_review(&mut app, review_source); + focus_review(&app, &mut mounted); + let before = parent_fields(&app, &mut mounted); + let raw = app.behavior.recovery_snapshot().unwrap(); + let source = app.document.bundle().clone(); + mounted.operate(&app, apply.as_mut()); + assert!(matches!(apply.finish(), widget::operation::Outcome::None)); + assert_eq!(parent_fields(&app, &mut mounted), before); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn scenario_form_focus_missing_exact_target_leaves_other_inputs_untouched() { + let (app, _, _) = focus_fixture(); + let stamp = app.scenario_focus_owner().unwrap(); + let mut view: iced::Element<'_, (), Theme, ()> = focus::root( + text_input("Unrelated input", "still focused") + .id("unrelated") + .on_input(|_| ()), + Some(stamp), + ); + let mut tree = widget::Tree::new(view.as_widget()); + let node = view.as_widget_mut().layout( + &mut tree, + &(), + &layout::Limits::new(Size::ZERO, Size::new(320., 80.)), + ); + view.as_widget_mut().operate( + &mut tree, + Layout::new(&node), + &(), + &mut FocusIndex { + target: 0, + index: 0, + }, + ); + let mut operation = focus_action(focus::task(stamp)).await; + view.as_widget_mut() + .operate(&mut tree, Layout::new(&node), &(), operation.as_mut()); + assert!(matches!( + operation.finish(), + widget::operation::Outcome::None + )); + let mut fields = Focused::default(); + view.as_widget_mut() + .operate(&mut tree, Layout::new(&node), &(), &mut fields); + assert_eq!(fields.0, vec![(Some(widget::Id::new("unrelated")), true)]); +} + +#[test] +fn scenario_workspace_viewport_events_are_passive_guarded_and_do_not_apply_raw_forms() { + use super::super::direct_tests::{fixture, navigation_event}; + let f = fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let _ = app.update(event(Event::Start(f.owner, Tool::Message))); + let _ = app.update(event(Event::Name(" raw 雨 ".into()))); + let raw = app.behavior.recovery_snapshot().unwrap(); + let accepted = app.document.snapshot(); + let context = app.scenario_context().unwrap(); + app.busy = true; + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport( + context, + Size::new(500., 200.), + ))); + assert_ne!(app.scenario_context(), Some(context)); + let epoch = app.canvas_epoch; + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport( + context, + Size::new(500., 200.), + ))); + assert_eq!(app.canvas_epoch, epoch); + for mismatch in 0..3 { + let mut stale = context; + match mismatch { + 0 => stale.document = Id::new_v4(), + 1 => stale.scenario = Id::new_v4(), + _ => stale.revision += 1, + } + let _ = app.update(Message::ScenarioCanvas(CanvasEvent::Viewport( + stale, + Size::new(900., 400.), + ))); + assert_eq!(app.canvas_epoch, epoch); + } + app.busy = false; + navigation_event(&mut app, |ctx| CanvasEvent::Shelf(ctx, Shelf::Tools)); + navigation_event(&mut app, CanvasEvent::Focus); + navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None)); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert!(!app.document.can_undo()); +} + +#[test] +fn scenario_workspace_choosing_tools_preserves_valid_and_invalid_pending_editors() { + use super::super::direct_tests::{fixture, navigation_event}; + for editor in 0..3 { + for valid in [false, true] { + let f = fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + match editor { + 0 => { + let _ = app.update(Message::Field( + if valid { "/name" } else { "/id" }.into(), + if valid { + " Retained property 雨 " + } else { + "not-a-uuid" + } + .into(), + )); + assert!(app.draft_dirty()); + assert_eq!(app.draft.as_ref().unwrap().command().is_ok(), valid); + } + 1 => { + let raw = if valid { + format!("// Retained source 雨\n{}", app.source_editor.text()) + } else { + "? invalid source 雨".into() + }; + let _ = app.update(Message::SourceEdit( + iced::widget::text_editor::Action::SelectAll, + )); + let _ = app.update(Message::SourceEdit( + iced::widget::text_editor::Action::Edit( + iced::widget::text_editor::Edit::Paste(std::sync::Arc::new(raw)), + ), + )); + assert!(app.source_dirty()); + assert_eq!( + crate::source_workspace::compile(&app.source_workspace.bundle()).is_ok(), + valid + ); + } + _ => { + let _ = app.update(event(Event::Start( + f.owner, + if valid { Tool::Fragment } else { Tool::Action }, + ))); + let _ = app.update(event(Event::Name(" Retained behavior 雨 ".into()))); + let _ = app.update(event(Event::Expression("ready".into()))); + let _ = app.update(event(Event::SecondGuard("otherwise".into()))); + assert!(app.behavior.has_pending()); + assert_eq!( + app.behavior + .scenario + .draft + .as_ref() + .unwrap() + .command(app.project()) + .is_ok(), + valid + ); + } + } + let source = serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap(); + let properties = app + .draft + .as_ref() + .map(|draft| serde_json::to_value(draft.recovery_snapshot()).unwrap()); + let behavior = app.behavior.recovery_snapshot().unwrap(); + let text = app.source_editor.text(); + let accepted = app.document.snapshot(); + let selected = app.selected; + let context = app.scenario_context(); + let tool = app.behavior.scenario.canvas_tool(f.owner); + navigation_event(&mut app, |ctx| { + CanvasEvent::Choose(ctx, CanvasTool::Message(MessageStyle::Timer)) + }); + assert!( + app.status.contains("Apply or cancel"), + "editor={editor} valid={valid}: {}", + app.status + ); + assert_eq!(app.source_editor.text(), text); + assert_eq!( + serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap(), + source + ); + assert_eq!( + app.draft + .as_ref() + .map(|draft| serde_json::to_value(draft.recovery_snapshot()).unwrap()), + properties + ); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), behavior); + assert_eq!(app.selected, selected); + assert_eq!(app.scenario_context(), context); + assert_eq!(app.behavior.scenario.canvas_tool(f.owner), tool); + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert!(!app.document.can_undo()); + assert!(!app.document.can_redo()); + } + } +} + +#[test] +fn scenario_workspace_scaled_and_scrolled_scene_keeps_clicked_slots_and_message_endpoints() { + let f = super::super::direct_tests::fixture(); + for scale in [0.25, 0.5, 1., 2.] { + for drag in [false, true] { + let mut scene = Scene::sequence(&f.project, f.owner, f.call); + let context = super::super::direct_tests::context(f.owner); + scene.scale = scale; + scene.scroll_offset = Vector::new(35., 40.); + scene.interaction = Some((context, CanvasTool::Message(MessageStyle::CallReply), true)); + let slot = scene.slots.last().unwrap(); + let expected = slot.slot; + let bounds = Rectangle::new( + Point::new(20., 80.), + Size::new(scene.size.width * scale, scene.size.height * scale), + ); + let screen = |world: Point| { + Point::new(bounds.x + world.x * scale, bounds.y + world.y * scale) + - scene.scroll_offset + }; + let a = screen(Point::new(scene.lanes[&f.sender], slot.y)); + let b = screen(Point::new(scene.lanes[&f.receiver], slot.y)); + let mut input = super::super::super::SequenceInput::default(); + let mut messages = vec![]; + let mut events = vec![ + (mouse::Event::CursorMoved { position: a }, a), + (mouse::Event::ButtonPressed(mouse::Button::Left), a), + ]; + if !drag { + events.push((mouse::Event::ButtonReleased(mouse::Button::Left), a)); + } + events.push((mouse::Event::CursorMoved { position: b }, b)); + if !drag { + events.push((mouse::Event::ButtonPressed(mouse::Button::Left), b)); + } + events.push((mouse::Event::ButtonReleased(mouse::Button::Left), b)); + for (event, cursor) in events { + if let Some(action) = canvas::Program::update( + &scene, + &mut input, + &canvas::Event::Mouse(event), + bounds, + mouse::Cursor::Available(cursor + scene.scroll_offset), + ) && let Some(message) = action.into_inner().0 + { + messages.push(message); + } + } + assert!( + matches!(messages.as_slice(), [Message::ScenarioCanvas(CanvasEvent::Begin { sender, receiver, slot, .. })] + if *sender == f.sender && *receiver == f.receiver && *slot == expected), + "scale={scale} drag={drag}" + ); + } + } +} + +#[test] +fn scenario_workspace_scaled_nested_occurrences_and_reorder_keep_exact_sequence_identity() { + let (f, branch, anchor, _, _, _) = super::super::direct_tests::placement_fixture(); + for scale in [0.25, 0.5, 1., 2.] { + let mut scene = Scene::sequence(&f.project, f.owner, f.call); + let context = super::super::direct_tests::context(f.owner); + scene.scale = scale; + scene.scroll_offset = Vector::new(35., 40.); + let bounds = Rectangle::new( + Point::new(20., 80.), + Size::new(scene.size.width * scale, scene.size.height * scale), + ); + let screen = |world: Point| { + Point::new(bounds.x + world.x * scale, bounds.y + world.y * scale) + - Vector::new(35., 40.) + }; + let send = + |scene: &Scene, input: &mut super::super::super::SequenceInput, event, cursor| { + canvas::Program::update( + scene, + input, + &canvas::Event::Mouse(event), + bounds, + mouse::Cursor::Available(cursor + scene.scroll_offset), + ) + .and_then(|action| action.into_inner().0) + }; + for tool in [Tool::Action, Tool::Observation, Tool::Reuse, Tool::Fragment] { + scene.interaction = Some((context, CanvasTool::Add(tool), true)); + for expected in [ + Slot { + parent: f.owner, + before: Some(f.reply), + }, + Slot { + parent: branch, + before: Some(anchor), + }, + ] { + let slot = scene + .slots + .iter() + .find(|slot| slot.slot == expected) + .unwrap(); + let point = screen(Point::new(scene.lanes[&f.sender], slot.y)); + let mut input = super::super::super::SequenceInput::default(); + send( + &scene, + &mut input, + mouse::Event::CursorMoved { position: point }, + point, + ); + assert!( + send( + &scene, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point + ) + .is_none() + ); + let message = send( + &scene, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + point, + ) + .unwrap(); + assert!( + matches!(message, Message::ScenarioCanvas(CanvasEvent::BeginOccurrence { tool: captured, slot, lifeline, .. }) + if captured == tool && slot == expected && lifeline == tool.uses_lifeline().then_some(f.sender)), + "scale={scale} tool={tool:?} slot={expected:?}" + ); + } + } + scene.interaction = Some((context, CanvasTool::Select, true)); + let mut input = super::super::super::SequenceInput::default(); + let expected = scene + .slots + .iter() + .rev() + .find(|slot| slot.slot.parent == f.owner) + .unwrap(); + let start = screen(scene.reorder_handle().unwrap().center()); + let end = screen(Point::new(17., expected.y)); + send( + &scene, + &mut input, + mouse::Event::CursorMoved { position: start }, + start, + ); + send( + &scene, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + start, + ); + send( + &scene, + &mut input, + mouse::Event::CursorMoved { position: end }, + end, + ); + assert!( + matches!(send(&scene, &mut input, mouse::Event::ButtonReleased(mouse::Button::Left), end), + Some(Message::ScenarioCanvas(CanvasEvent::Reorder { occurrence, slot, .. })) + if occurrence == f.call && slot == expected.slot), + "scale={scale}" + ); + } +} + +#[tokio::test] +async fn production_scenario_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags() +{ + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + for boundary in ["Fit", "A", "Details", "Panels", "Bounds"] { + let toolbar = capture_toolbar(&mut mounted, &app, "Fit"); + let bounds = mounted.geometry(&app).bounds("scenario-viewport"); + let point = bounds.position() + iced::Vector::new(95., 90.); + let lifetime = app.camera_epoch; + let pan = app.behavior.scenario.presentation.pan; + let first = point + iced::Vector::new(13., 9.); + let last = first + iced::Vector::new(17., -3.); + let emitted = mounted + .batch( + &mut app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)), + iced::Event::Mouse(mouse::Event::CursorMoved { position: first }), + iced::Event::Mouse(mouse::Event::CursorMoved { position: last }), + ], + mouse::Cursor::Available(last), + ) + .await; + assert_eq!(app.behavior.scenario.presentation.pan, pan + (last - point)); + assert_eq!( + app.camera_epoch, lifetime, + "own ordered deltas retain their lifetime" + ); + let retained = camera_delta(&emitted); + if boundary == "Bounds" { + let size = Size::new(app.window_size.width + 90., app.window_size.height + 35.); + let task = app.update(Message::WindowResized(size)); + mounted.perform(&mut app, task).await; + } else { + let id = scope(&app).control_id(boundary); + let at = mounted.geometry(&app).containers[&id].center(); + let clicked = mounted.click(&mut app, at).await; + assert!( + focused(&mut mounted, &app, boundary), + "same-batch pointer activation focuses the resulting live control without F6" + ); + if boundary == "Fit" { + mounted + .batch( + &mut app, + &[key(iced::keyboard::key::Named::Tab)], + mouse::Cursor::Unavailable, + ) + .await; + assert!( + focused(&mut mounted, &app, "ZoomOut"), + "Tab after the real press/release batch continues from Fit" + ); + assert!(clicked.iter().any(|m| matches!( + intent_action(m), + Message::ScenarioCanvas(CanvasEvent::Zoom(_, None)) + ))); + assert_eq!( + app.behavior.scenario.presentation.pan, + iced::Vector::ZERO, + "Fit emitted after this same batch's old pan must still execute" + ); + } + } + mounted.settle(&mut app).await; + assert_ne!( + app.camera_epoch, lifetime, + "{boundary} ends the old input lifetime" + ); + let camera = ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom, + ); + let bounds = mounted.geometry(&app).bounds("scenario-viewport"); + let released = release_middle( + &mut mounted, + &mut app, + bounds.position() + iced::Vector::new(130., 145.), + ) + .await; + assert!(!released.iter().any(|m| matches!(m, Message::ScenarioCanvas(CanvasEvent::Camera(_, _, crate::behavior::canvas_workspace::Event::Pan(delta))) if *delta != iced::Vector::ZERO)), + "{boundary} must cancel the retained widget drag"); + let task = app.update(retained); + mounted.perform(&mut app, task).await; + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera, + "{boundary} must reject its pre-boundary App delta" + ); + let current_lifetime = app.camera_epoch; + let task = app.update(toolbar); + mounted.perform(&mut app, task).await; + assert_eq!( + app.camera_epoch, current_lifetime, + "{boundary} rejects the retained toolbar lifetime" + ); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera, + "{boundary} retained Fit cannot alter the camera" + ); + } + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_scenario_keyboard_enters_activates_draws_focus_and_returns_to_inspector() { + use crate::behavior::canvas_workspace::tests::*; + use iced::keyboard::key::Named; + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + assert!(!focused(&mut mounted, &app, "viewport")); + let operations = mounted.operations; + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!( + mounted.operations > operations, + "entry executes the returned App widget Task" + ); + assert!(focused(&mut mounted, &app, "viewport")); + let mut backwards = key(Named::Tab); + if let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { modifiers, .. }) = + &mut backwards + { + *modifiers = iced::keyboard::Modifiers::SHIFT; + } + mounted + .batch(&mut app, &[backwards], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "ActualSize")); + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "viewport")); + assert_eq!( + mounted + .geometry(&app) + .focused + .iter() + .filter(|(_, _, focused)| *focused) + .count(), + 1 + ); + tab_to(&mut mounted, &mut app, "A").await; + let chosen = mounted + .batch(&mut app, &[key(Named::Enter)], mouse::Cursor::Unavailable) + .await; + assert!(chosen.iter().any(|m| matches!( + intent_action(m), + Message::ScenarioCanvas(CanvasEvent::Choose(..)) + ))); + assert!( + focused(&mut mounted, &app, "A"), + "tool activation retains its keyboard target" + ); + // The selected palette tool must still have a visibly distinct focus ring. + let tool_on = mounted.pixels(&app); + let geometry = mounted.geometry(&app); + let mut tool_bounds = geometry.containers[&scope(&app).control_id("A")]; + let offset = geometry.scrolls[&scope(&app).control_id("palette-scroll")].1; + tool_bounds.x -= offset.x; + tool_bounds.y -= offset.y; + mounted + .batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + let tool_off = mounted.pixels(&app); + assert_drawn_focus(&tool_off, &tool_on, app.window_size, tool_bounds); + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + // Focus alone changes the visible border of the real production Fit control. + let before = mounted.pixels(&app); + tab_to(&mut mounted, &mut app, "Fit").await; + let id = scope(&app).control_id("Fit"); + let bounds = mounted.geometry(&app).containers[&id]; + let after = mounted.pixels(&app); + assert_drawn_focus(&before, &after, app.window_size, bounds); + let fitted = mounted + .batch(&mut app, &[key(Named::Space)], mouse::Cursor::Unavailable) + .await; + assert!(fitted.iter().any(|m| matches!( + intent_action(m), + Message::ScenarioCanvas(CanvasEvent::Zoom(_, None)) + ))); + assert_eq!(app.behavior.scenario.presentation.pan, iced::Vector::ZERO); + assert!(focused(&mut mounted, &app, "Fit")); + tab_to(&mut mounted, &mut app, "Canvas").await; + mounted + .batch(&mut app, &[key(Named::Enter)], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "viewport")); + mounted + .batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + assert!(!focused(&mut mounted, &app, "viewport")); + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + assert!( + mounted + .geometry(&app) + .focused + .iter() + .any(|(_, _, focused)| *focused), + "ordinary Tab resumes the production Inspector's own entry policy" + ); + assert!(!focused(&mut mounted, &app, "viewport")); + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "viewport")); + assert_eq!( + mounted + .geometry(&app) + .focused + .iter() + .filter(|(_, _, focused)| *focused) + .count(), + 1, + "explicit canvas entry releases Inspector keyboard ownership" + ); + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!(!focused(&mut mounted, &app, "viewport")); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + raw + ); + // Typing after exit belongs to Inspector. Replaying the prior clean tool + // action must retain those raw fields, never implicitly Apply them. + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + mounted + .batch(&mut app, &[character("x")], mouse::Cursor::Unavailable) + .await; + assert!(app.draft_dirty()); + let dirty_raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + assert_ne!(dirty_raw, raw); + let camera = ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom, + ); + for message in chosen { + let task = app.update(message); + mounted.perform(&mut app, task).await; + } + let emitted = mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!( + !emitted + .iter() + .any(|m| matches!(m, Message::ScenarioCanvas(CanvasEvent::Camera(..)))) + ); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera + ); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + dirty_raw + ); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_scenario_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes() { + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1440., 900.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + retained_toolbar_boundaries(&mut mounted, &mut app, "A").await; + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_scenario_keyboard_reveals_and_activates_initially_offscreen_last_tool() { + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 420.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + overflow_last_tool(&mut mounted, &mut app, "alt").await; + assert_eq!( + app.behavior.scenario.canvas_tool(f.owner), + CanvasTool::Add(Tool::Fragment) + ); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +// Successor to the former 2000px synthetic form / scrollable-canvas fixture. +// The real editor clamps the 700px request; that exact measured offset must +// survive same-session edits and reset to zero on replacement without a closed render. +#[tokio::test] +async fn new_scenario_form_resets_only_its_scroll_and_recovery_gets_a_fresh_session() { + let f = super::super::direct_tests::fixture(); + let mut app = App::with_project(f.project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 420.); + let mut mounted = Mounted::new(&mut app).await; + let canvas = mounted.geometry(&app).bounds("scenario-viewport"); + let start = canvas.position() + Vector::new(90., 80.); + mounted + .pan(&mut app, start, start + Vector::new(-137., -850.)) + .await; + let camera = ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom, + ); + assert_eq!(camera.0, Vector::new(-137., -850.)); + let task = app.update(event(Event::Start(f.owner, Tool::Action))); + mounted.perform(&mut app, task).await; + mounted.settle(&mut app).await; + let first = app.behavior.scenario.form_session().unwrap(); + let invocation = app.behavior.scenario.draft.as_ref().unwrap().invocation; + mounted.scroll(&app, "scenario-form", 700.); + let offset = mounted.geometry(&app).scrolls[&widget::Id::new("scenario-form")].1; + assert!( + offset.y > 64., + "real Action editor must actually overflow: {offset:?}" + ); + let accepted = app.document.snapshot(); + let source = app.document.bundle().clone(); + let task = app.update(event(Event::Name(" retained raw 雨 ".into()))); + mounted.perform(&mut app, task).await; + let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap(); + assert_eq!( + mounted.geometry(&app).scrolls[&widget::Id::new("scenario-form")].1, + offset + ); + assert_eq!(app.behavior.scenario.form_session(), Some(first)); + assert_eq!( + app.behavior.scenario.draft.as_ref().unwrap().invocation, + invocation + ); + // No render or widget operation between Cancel and Start. + let cancel = app.update(event(Event::Cancel)); + let start = app.update(event(Event::Start(f.owner, Tool::Observation))); + mounted + .perform(&mut app, iced::Task::batch([cancel, start])) + .await; + assert_ne!(app.behavior.scenario.form_session(), Some(first)); + let replaced = mounted.geometry(&app); + assert_eq!( + replaced.scrolls[&widget::Id::new("scenario-form")].1, + Vector::ZERO + ); + assert_eq!(replaced.bounds("scenario-viewport"), canvas); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera + ); + let recovered = State::from_recovery(raw.clone()).unwrap(); + assert_ne!(recovered.form_session(), Some(first)); + assert_eq!( + recovered.draft.as_ref().unwrap().name, + " retained raw 雨 " + ); + assert_eq!(recovered.draft.as_ref().unwrap().invocation, invocation); + let recovered_raw = recovered.recovery_snapshot().unwrap().unwrap(); + for field in [ + "name", + "owner", + "parent", + "before", + "target", + "source_origin", + "invocation", + ] { + assert_eq!(recovered_raw[field], raw[field], "recovered {field}"); + } + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[test] +fn scenario_workspace_fit_uses_measured_inner_viewport_and_rejects_nonfinite_inputs() { + let graph = Size::new(900., 1400.); + let mut presentation = Presentation::default(); + assert_eq!(presentation.scale(graph), 1.); + presentation.zoom(None); + assert_eq!( + presentation.scale(graph), + 1., + "no invented window-height fallback" + ); + assert!(presentation.viewport(Size::new(484., 204.))); + assert!(!presentation.viewport(Size::new(484., 204.))); + let scale = presentation.scale(graph); + assert!(graph.width * scale <= 468. && graph.height * scale <= 188.); + assert!(scale > MIN_SCALE); + for bad in [f32::NAN, f32::INFINITY, 0., -1.] { + assert!(!presentation.zoom(Some(bad))); + assert!(!presentation.viewport(Size::new(bad, 100.))); + assert!(!presentation.viewport(Size::new(100., bad))); + assert_eq!(presentation.scale(graph), scale); + } + assert_eq!(presentation.scale(Size::new(900., 10000.)), MIN_SCALE); + assert!(presentation.zoom(Some(1.25))); + presentation.viewport(Size::new(200., 100.)); + assert_eq!( + presentation.scale(graph), + 1.25, + "manual zoom survives form/viewport resize" + ); +} + +#[tokio::test] +async fn scenario_workspace_same_size_owner_and_document_changes_remeasure_fit_viewport() { + use super::super::direct_tests::{navigation_event, navigation_fixture}; + let (project, [a, b, _], [reference, _, _]) = navigation_fixture(); + let mut app = App::with_project(project.clone()); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1440., 900.); + let mut mounted = Mounted::new(&mut app).await; + let first = mounted.geometry(&app).bounds("scenario-viewport").size(); + assert_eq!(app.behavior.scenario.presentation.viewport, Some(first)); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, reference)); + assert_eq!(app.scenario_context().unwrap().scenario, b); + assert!(app.behavior.scenario.presentation.viewport.is_none()); + mounted.settle(&mut app).await; + assert_eq!(app.behavior.scenario.presentation.viewport, Some(first)); + let graph = Scene::sequence(app.project(), b, b).size; + navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None)); + let fitted = app.behavior.scenario.presentation.scale(graph); + assert!( + graph.width * fitted <= first.width - 16. && graph.height * fitted <= first.height - 16. + ); + assert_eq!(app.behavior.scenario.presentation.pan, Vector::ZERO); + let prior_document = app.scenario_context().unwrap().document; + app = App::with_project(project); + let _ = app.update(Message::Select(b)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1440., 900.); + let context = app.scenario_context().unwrap(); + assert_ne!(context.document, prior_document); + assert_eq!( + context.scenario, b, + "semantic UUID is retained across documents" + ); + assert!(app.behavior.scenario.presentation.viewport.is_none()); + let accepted = app.document.snapshot(); + let source = app.document.bundle().clone(); + mounted.settle(&mut app).await; + assert_eq!(app.behavior.scenario.presentation.viewport, Some(first)); + navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, None)); + assert_eq!(app.behavior.scenario.presentation.scale(graph), fitted); + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +// Real mounted counterpart of both former synthetic caller-scroll fixtures. +#[tokio::test] +async fn scenario_workspace_open_and_back_restore_owner_and_mounted_camera_after_preselection() { + use super::super::direct_tests::{navigation_event, navigation_fixture}; + let (mut p, [a, b, _], [reference, _, _]) = navigation_fixture(); + // Give the real Details overlay enough legitimate rows to test its offset. + for index in 0..30 { + let element = super::super::super::topology::occurrence( + &p, + a, + &format!("Additional use {index}"), + OccurrenceBody::ScenarioUse { + scenario: Some(b), + bindings: vec![], + }, + ); + p = execute_batch(&p, &[Command::CreateElement(element)]).unwrap(); + } + let mut app = App::with_project(p); + let _ = app.update(Message::Select(a)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + navigation_event(&mut app, |ctx| { + CanvasEvent::Choose(ctx, CanvasTool::Message(MessageStyle::Timer)) + }); + navigation_event(&mut app, |ctx| CanvasEvent::Shelf(ctx, Shelf::Selection)); + navigation_event(&mut app, CanvasEvent::Focus); + let mut mounted = Mounted::new(&mut app).await; + navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, Some(0.75))); + let canvas = mounted.geometry(&app).bounds("scenario-viewport"); + let point = canvas.position() + Vector::new(90., 120.); + mounted + .pan(&mut app, point, point + Vector::new(90., 180.)) + .await; + let camera = ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom, + ); + mounted.scroll(&app, "scenario-details", 80.); + mounted.settle(&mut app).await; + let details = mounted.geometry(&app).scrolls[&widget::Id::new("scenario-details")].1; + assert_eq!(details, Vector::new(0., 80.)); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera, + "Details is not a camera ancestor" + ); + let _ = app.update(Message::Command(Command::Rename( + b, + "Renamed definition".into(), + ))); + assert_eq!(app.selected, b); + assert_eq!(app.scenario_context().unwrap().scenario, a); + let before_open = app.document.snapshot(); + let source_open = app.document.bundle().clone(); + navigation_event(&mut app, |ctx| CanvasEvent::OpenUse(ctx, reference)); + assert_eq!(app.selected, b); + assert_eq!(app.scenario_context().unwrap().scenario, b); + assert!(std::sync::Arc::ptr_eq( + &before_open, + &app.document.snapshot() + )); + assert_eq!(app.document.bundle(), &source_open); + mounted.settle(&mut app).await; + assert_eq!(app.behavior.scenario.presentation.pan, Vector::ZERO); + navigation_event(&mut app, |ctx| CanvasEvent::Zoom(ctx, Some(2.))); + navigation_event(&mut app, CanvasEvent::Focus); + let _ = app.update(Message::Command(Command::Rename( + reference, + "Renamed caller use".into(), + ))); + assert_eq!(app.selected, reference); + assert_eq!(app.scenario_context().unwrap().scenario, b); + let before_back = app.document.snapshot(); + let source_back = app.document.bundle().clone(); + let revision = app.document.revision(); + let bindings = app.project().elements[&reference].clone(); + navigation_event(&mut app, CanvasEvent::Back); + assert_eq!(app.selected, reference); + assert_eq!(app.scenario_context().unwrap().scenario, a); + assert_eq!( + app.behavior.scenario.canvas_tool(a), + CanvasTool::Message(MessageStyle::Timer) + ); + assert!(app.behavior.scenario.focused()); + assert_eq!( + app.behavior.scenario.presentation.shelf, + Some(Shelf::Selection) + ); + assert_eq!( + ( + app.behavior.scenario.presentation.pan, + app.behavior.scenario.presentation.zoom + ), + camera + ); + assert_eq!(app.behavior.scenario.presentation.zoom, Some(0.75)); + mounted.settle(&mut app).await; + assert_eq!( + mounted.geometry(&app).scrolls[&widget::Id::new("scenario-details")].1, + details + ); + assert_eq!( + app.behavior.scenario.scroll_offset(), + Vector::ZERO, + "canvas uses camera pan, not a synthetic Scrollable offset" + ); + for area in [ScrollArea::Canvas, ScrollArea::Page] { + assert!( + app.behavior + .scenario + .scroll_restore(app.scenario_context().unwrap(), area) + .is_none() + ); + } + assert!(app.behavior.scenario.restore.is_none()); + assert!(std::sync::Arc::ptr_eq( + &before_back, + &app.document.snapshot() + )); + assert_eq!(app.document.bundle(), &source_back); + assert_eq!(app.project().elements[&reference], bindings); + assert_eq!(app.document.revision(), revision); + assert!(app.document.can_undo()); + assert!(!app.document.can_redo()); + mounted + .pan(&mut app, point, point + Vector::new(0., 40.)) + .await; + mounted.settle(&mut app).await; + assert_eq!( + app.behavior.scenario.presentation.pan, + camera.0 + Vector::new(0., 40.), + "a consumed restore receipt cannot snap a later gesture back" + ); +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology.rs new file mode 100644 index 00000000..1fcbce81 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology.rs @@ -0,0 +1,2229 @@ +//! Process/chain authoring and projection use contextual occurrences and explicit +//! SequenceLinks. Presentation order never invents control-flow relationships. +use super::*; +use iced::widget::{Column, text_input}; +use std::collections::BTreeSet; +pub(crate) mod contextual; +mod direct; +mod view; +use direct::Input; +pub(crate) use direct::{ + CanvasEvent, CanvasTool, Context, Insertion, reconnect, referenced, reorder, +}; +#[cfg(test)] +pub(crate) mod direct_tests; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum Tool { + Work, + Exchange, + Reuse, + Control, + Connect, + Branch, + Contextual, +} +impl Tool { + fn title(self) -> &'static str { + match self { + Self::Work => "Involve work", + Self::Exchange => "Involve exchange", + Self::Reuse => "Reuse process / chain", + Self::Control => "Add control", + Self::Connect => "Connect contributions", + Self::Branch => "Insert two paths", + Self::Contextual => "Connect qualified contributions", + } + } +} +#[derive(Debug, Clone)] +pub enum Event { + Source(super::source_draft::Event), + Start(Id, Tool, Option), + Cancel, + Submit, + Name(String), + Target(Choice), + Alternate(Choice), + Before(Choice), + After(Choice), + ClearBefore, + ClearAfter, + Guard(String), + AlternateGuard(String), + Control(ControlChoice), + Search(usize, String), + Page(usize, usize), + Contextual(contextual::Event), + Shelf(Shelf), +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Shelf { + Tools, + Selection, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ControlChoice(pub ControlKind); +impl std::fmt::Display for ControlChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self.0 { + ControlKind::Choice => "OR · alternative paths", + ControlKind::Merge => "OR · merge alternatives", + ControlKind::Fork => "AND · parallel paths", + ControlKind::Join => "AND · join parallel paths", + ControlKind::Loop => "IT · repeat / exit", + }) + } +} +#[derive(Default)] +pub struct State { + identity: Option<(Id, Id)>, + tool: CanvasTool, + expanded: BTreeSet, + scroll_offsets: [iced::Vector; 2], + return_to: Vec, + draft: Option, + viewport: Option, + pan: iced::Vector, + zoom: Option, + fit: bool, + pub(crate) focused: bool, + contextual_expanded: BTreeSet, + selected_context: Option, + shelf: Option, +} +#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct Browser { + search: String, + page: usize, +} +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +struct Draft { + #[serde(default)] + source: super::source_draft::State, + #[serde(default = "Id::new_v4")] + invocation: Id, + #[serde(default)] + source_origin: Option<(Id, oa_core::Element)>, + + #[serde(with = "recovery_insertion")] + insertion: Insertion, + fixed: bool, + browsers: [Browser; 4], + owner: Id, + tool: Tool, + name: String, + target: Option, + alternate: Option, + before: Option, + after: Option, + guard: String, + alternate_guard: String, + control: ControlKind, + #[serde(default)] + contextual: Option, +} +impl State { + pub(crate) fn camera_boundary(&self) -> impl PartialEq + use<> { + ( + self.tool, + self.focused, + self.shelf, + self.viewport, + self.pan, + self.zoom, + self.fit, + ) + } + + pub(super) fn pending_name(&self) -> Option<&str> { + self.draft.as_ref().map(|d| d.name.as_str()) + } + pub(super) fn source_view<'a>(&'a self, p: &'a Project) -> Option> { + self.draft + .as_ref()? + .source + .view(p) + .map(|v| v.map(Event::Source)) + } + + pub fn source_active(&self) -> bool { + self.draft.as_ref().is_some_and(|d| d.source.engaged()) + } + + pub fn source_open(&self) -> bool { + self.draft.as_ref().is_some_and(|d| d.source.is_open()) + } + pub fn source_back(&mut self) -> bool { + self.draft.as_mut().is_some_and(|d| d.source.back()) + } + + pub(super) fn recovery_snapshot(&self) -> Result, String> { + self.draft + .as_ref() + .map(serde_json::to_value) + .transpose() + .map_err(|e| e.to_string()) + } + + pub(super) fn from_recovery(value: serde_json::Value) -> Result { + let draft = serde_json::from_value(value).map_err(|e| e.to_string())?; + Ok(Self { + draft: Some(draft), + ..Default::default() + }) + } + + pub(super) fn pending_owner(&self) -> Option { + self.draft.as_ref().map(|d| d.owner) + } + + pub(super) fn prepare_pending(&self, p: &Project) -> Result { + let command = self + .draft + .as_ref() + .ok_or("Choose a contribution tool first.")? + .command(p)?; + execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?; + Ok(command) + } + + pub(super) fn acknowledge_commit(&mut self) { + self.draft = None; + } + + pub fn update(&mut self, p: &Project, event: Event) -> Result, String> { + if let Event::Source(event) = event { + if self + .draft + .as_mut() + .ok_or("Start the consumer form first.")? + .source + .dismiss(&event)? + { + return Ok(None); + } + let d = self + .draft + .as_ref() + .ok_or("Start the consumer form first.")?; + if d.tool != Tool::Work { + return Err("This reference does not support staged source creation.".into()); + } + if d.source_origin.as_ref().is_some_and(|(document, owner)| { + *document != p.id || p.elements.get(&d.owner) != Some(owner) + }) { + return Err("The original consumer changed. Cancel and reopen its form; the current draft is retained.".into()); + } + if d.source_origin.is_none() && d.source.engaged() { + return Err("The recovered source draft has no original document binding. Go Back or cancel it.".into()); + } + let mut next = d.clone(); + if next.source_origin.is_none() && matches!(&event, super::source_draft::Event::Open(_)) + { + next.source_origin = Some(( + p.id, + p.elements + .get(&d.owner) + .ok_or("The consumer owner no longer exists.")? + .clone(), + )); + } + if let Some(id) = next.source.update( + p, + super::source_draft::Scope::Work { owner: d.owner }, + d.invocation, + event, + )? { + next.target = Some(id); + next.command(p)?; + } + self.draft = Some(next); + return Ok(None); + } + if self.source_open() && !matches!(&event, Event::Cancel) { + return Err("Use the new source or go Back before editing the parent form.".into()); + } + + if self.draft.is_some() && matches!(&event, Event::Start(..)) { + return Err(PENDING_DRAFT.into()); + } + match event { + Event::Shelf(shelf) => { + self.shelf = (self.shelf != Some(shelf)).then_some(shelf); + } + Event::Start(owner, tool, selected) => { + let e = p + .elements + .get(&owner) + .ok_or("This process or chain no longer exists.")?; + if !matches!(e.kind(), Kind::Process | Kind::FunctionalChain) { + return Err("Choose an operational process or system functional chain.".into()); + } + let before = selected.filter(|id| is_contribution(p, owner, *id)); + let after = before.and_then(|id| { + let outgoing: Vec<_> = sequence_links(p, owner) + .into_iter() + .filter(|r| r.source() == id) + .map(Relation::target) + .collect(); + if outgoing.len() == 1 { + outgoing.first().copied() + } else { + legacy_order(p, owner) + .windows(2) + .find(|pair| pair[0] == id) + .map(|pair| pair[1]) + } + }); + self.draft = Some(Draft { + source: Default::default(), + invocation: Id::new_v4(), + source_origin: Some((p.id, p.elements[&owner].clone())), + insertion: Insertion::default(), + fixed: false, + browsers: Default::default(), + owner, + tool, + name: String::new(), + target: None, + alternate: None, + before, + after, + guard: String::new(), + alternate_guard: "else".into(), + control: ControlKind::Choice, + contextual: (tool == Tool::Contextual) + .then(|| contextual::Draft::new(p, owner, selected)), + }); + if let Some(endpoint) = self + .qualified_selection(p, owner) + .map(|address| address.endpoint.clone()) + && let Some(contextual) = + self.draft.as_mut().and_then(|d| d.contextual.as_mut()) + { + contextual.seed_source(endpoint); + } + } + Event::Cancel => self.acknowledge_commit(), + Event::Submit => return self.prepare_pending(p).map(Some), + other => { + if let Some(d) = &mut self.draft { + match other { + Event::Name(value) => d.name = value, + Event::Target(value) => d.target = Some(value.id), + Event::Alternate(value) => d.alternate = Some(value.id), + Event::Before(value) => d.before = Some(value.id), + Event::After(value) => d.after = Some(value.id), + Event::ClearBefore => d.before = None, + Event::ClearAfter => d.after = None, + Event::Guard(value) => d.guard = value, + Event::AlternateGuard(value) => d.alternate_guard = value, + Event::Control(value) => d.control = value.0, + Event::Search(index, value) if index < 4 => { + d.browsers[index].search = value; + d.browsers[index].page = 0; + } + Event::Page(index, value) if index < 4 => d.browsers[index].page = value, + Event::Contextual(event) => { + if let Some(contextual) = &mut d.contextual { + contextual.update(event); + } + } + _ => {} + } + } + } + } + Ok(None) + } + + fn scale(&self, graph: Size, fallback_width: f32) -> f32 { + if !self.fit { + return self.zoom.unwrap_or(1.); + } + let available = self.fit_area(fallback_width); + (available.width / graph.width.max(1.)) + .min(available.height / graph.height.max(1.)) + .clamp(0.05, 1.) + } + + fn fit_area(&self, fallback_width: f32) -> Size { + // The sensor measures the inner scroller, independent of graph content. + // Leave a small gutter for scrollbars; before first layout use a compact + // provisional viewport and recompute when the sensor reports actual size. + let viewport = self.viewport.unwrap_or(Size::new(fallback_width, 180.)); + Size::new( + (viewport.width - 24.).max(1.), + (viewport.height - 10.).max(1.), + ) + } + + #[cfg(test)] + fn canvas_offset(&self) -> iced::Vector { + self.scroll_offsets[0] + } + + #[cfg(test)] + fn fit_limited(&self, graph: Size, fallback_width: f32) -> bool { + let available = self.fit_area(fallback_width); + self.fit && (graph.width * 0.05 > available.width || graph.height * 0.05 > available.height) + } +} + +mod recovery_insertion { + use super::*; + use serde::{Deserialize, Serialize}; + pub fn serialize( + value: &Insertion, + serializer: S, + ) -> Result { + (value.link, value.before).serialize(serializer) + } + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result { + let (link, before) = <(Option, Option)>::deserialize(deserializer)?; + Ok(Insertion { link, before }) + } +} +fn is_contribution(p: &Project, owner: Id, id: Id) -> bool { + p.elements.get(&id).is_some_and(|e| { + e.parent == Some(owner) + && matches!( + e.shared(), + Some(SharedBody::Occurrence { + body: OccurrenceBody::Action { .. } + | OccurrenceBody::Exchange { .. } + | OccurrenceBody::Control { .. } + | OccurrenceBody::SequenceReference { .. }, + .. + }) + ) + }) +} +fn reference_reaches(p: &Project, from: Id, to: Id) -> bool { + let mut pending = vec![from]; + let mut seen = BTreeSet::new(); + while let Some(id) = pending.pop() { + if id == to { + return true; + } + if seen.insert(id) { + pending.extend(p.children(id).filter_map(|e| match e.shared() { + Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { sequence }, + .. + }) => *sequence, + _ => None, + })); + } + } + false +} +fn targets(p: &Project, owner: Id, tool: Tool) -> Vec { + let Some(e) = p.elements.get(&owner) else { + return vec![]; + }; + let mut choices: Vec<_> = if tool == Tool::Exchange { + p.relations + .values() + .filter(|r| { + p.layer_of(r.id) == Some(e.layer()) + && r.kind() + == if e.layer() == Layer::Oa { + RelationKind::Interaction + } else { + RelationKind::FunctionalExchange + } + }) + .map(|r| Choice { + id: r.id, + label: format!( + "{} · {} → {}", + r.name, + p.named(r.source()), + p.named(r.target()) + ), + }) + .collect() + } else { + p.elements + .values() + .filter(|candidate| { + candidate.layer() == e.layer() + && match tool { + Tool::Work | Tool::Branch => candidate.kind().is_work(), + Tool::Reuse => { + candidate.kind() == e.kind() + && !reference_reaches(p, candidate.id, owner) + } + _ => false, + } + }) + .map(|e| Choice { + id: e.id, + label: format!("{} · {}", e.name, semantic_path(p, e.id)), + }) + .collect() + }; + choices.sort_by(|a, b| a.label.cmp(&b.label)); + choices +} +fn semantic_path(p: &Project, id: Id) -> String { + let mut path = Vec::new(); + let mut current = Some(id); + while let Some(id) = current { + let Some(e) = p.elements.get(&id) else { break }; + path.push(e.alias.as_str()); + current = e.parent; + } + path.reverse(); + path.join(" / ") +} +fn contributions(p: &Project, owner: Id) -> Vec { + p.elements + .get(&owner) + .and_then(|e| e.steps()) + .into_iter() + .flatten() + .filter(|id| is_contribution(p, owner, **id)) + .enumerate() + .map(|(i, id)| Choice { + id: *id, + label: format!("{}. {}", i + 1, p.named(*id)), + }) + .collect() +} +fn legacy_order(p: &Project, owner: Id) -> Vec { + if uses_explicit_topology(p, owner) { + return vec![]; + } + let Some(steps) = p.elements.get(&owner).and_then(|e| e.steps()) else { + return vec![]; + }; + if steps.iter().all(|id| { + matches!( + p.elements.get(id).and_then(|e| e.shared()), + Some(SharedBody::Occurrence { + body: OccurrenceBody::Exchange { .. }, + .. + }) + ) + }) { + steps.clone() + } else { + vec![] + } +} +fn preserved_order(p: &Project, owner: Id, replace: Option<(Id, Id)>) -> Vec { + legacy_order(p, owner) + .windows(2) + .filter(|pair| replace != Some((pair[0], pair[1]))) + .map(|pair| link(p, pair[0], pair[1], "")) + .collect() +} +fn contribution_work(p: &Project, id: Id) -> Option { + match p.elements.get(&id)?.shared()? { + SharedBody::Occurrence { + body: OccurrenceBody::Action { action, .. }, + .. + } => *action, + _ => None, + } +} +fn contribution_exchange(p: &Project, id: Id) -> Option<&Relation> { + match p.elements.get(&id)?.shared()? { + SharedBody::Occurrence { + body: OccurrenceBody::Exchange { exchange, .. }, + .. + } => exchange.and_then(|id| p.relations.get(&id)), + _ => None, + } +} +fn compatible_source(p: &Project, draft: &Draft, id: Id) -> bool { + match draft.tool { + Tool::Work => { + draft + .before + .and_then(|id| contribution_exchange(p, id)) + .is_none_or(|r| r.target() == id) + && draft + .after + .and_then(|id| contribution_exchange(p, id)) + .is_none_or(|r| r.source() == id) + } + Tool::Exchange => { + let Some(exchange) = p.relations.get(&id) else { + return false; + }; + let preceding = draft.before.and_then(|id| { + contribution_work(p, id) + .or_else(|| contribution_exchange(p, id).map(Relation::target)) + }); + let following = draft.after.and_then(|id| { + contribution_work(p, id) + .or_else(|| contribution_exchange(p, id).map(Relation::source)) + }); + preceding.is_none_or(|id| exchange.source() == id) + && following.is_none_or(|id| exchange.target() == id) + } + _ => true, + } +} +pub(super) fn occurrence( + p: &Project, + owner: Id, + name: &str, + body: OccurrenceBody, +) -> oa_core::Element { + let mut e = p + .make_element( + Kind::Occurrence, + p.elements[&owner].layer(), + name, + Some(owner), + ) + .expect("Occurrence layer"); + if let Some(SharedBody::Occurrence { body: value, .. }) = e.shared_mut() { + *value = body; + } + e +} +fn link(p: &Project, source: Id, target: Id, guard: &str) -> Command { + let id = Id::new_v4(); + Command::CreateRelation(Relation { + id, + alias: p.fresh_alias("sequence_link", "relation", id), + name: "Sequence link".into(), + description: String::new(), + body: RelationBody::SequenceLink { + source, + target, + guard: guard.to_owned(), + }, + }) +} +impl Draft { + fn command(&self, p: &Project) -> Result { + if self.source.engaged() + && self.source_origin.as_ref().is_none_or(|(document, owner)| { + *document != p.id || p.elements.get(&self.owner) != Some(owner) + }) + { + return Err("The original consumer document or owner changed. Cancel and reopen its form; your draft is retained.".into()); + } + self.source.check_scope( + super::source_draft::Scope::Work { owner: self.owner }, + self.target, + )?; + let preview = self.source.preview(p, self.target)?; + let command = self.consumer_command(preview.as_ref().unwrap_or(p))?; + self.source.combine(p, self.target, command) + } + fn consumer_command(&self, p: &Project) -> Result { + let owner = p + .elements + .get(&self.owner) + .ok_or("The process or chain no longer exists.")?; + if !matches!(owner.kind(), Kind::Process | Kind::FunctionalChain) { + return Err("Choose a process or chain.".into()); + } + if (self.tool == Tool::Contextual) != self.contextual.is_some() { + return Err("The qualified link draft and selected tool do not agree.".into()); + } + if let Some(draft) = &self.contextual { + return draft.command(p, self.owner, &self.name); + } + for id in [self.before, self.after].into_iter().flatten() { + if !is_contribution(p, self.owner, id) { + return Err("Choose contributions from this process or chain.".into()); + } + } + if self.before.is_some() && self.before == self.after { + return Err("Choose different preceding and following contributions.".into()); + } + let captured = self + .insertion + .link + .map(|id| { + p.relations + .get(&id) + .filter(|r| { + r.kind() == RelationKind::SequenceLink + && is_contribution(p, self.owner, r.source()) + && is_contribution(p, self.owner, r.target()) + }) + .ok_or("The captured path no longer exists in this process.") + }) + .transpose()?; + if captured + .is_some_and(|r| Some(r.source()) != self.before || Some(r.target()) != self.after) + { + return Err("The captured path changed. Choose its position again.".into()); + } + let matches: Vec<_> = sequence_links(p, self.owner) + .into_iter() + .filter(|r| Some(r.source()) == self.before && Some(r.target()) == self.after) + .collect(); + let split_link = if self.tool == Tool::Connect { + None + } else if captured.is_some() { + captured + } else if !self.fixed && matches.len() == 1 { + Some(matches[0]) + } else if !self.fixed && matches.len() > 1 { + return Err("Select the specific sequence link to insert on.".into()); + } else { + None + }; + let mut commands = preserved_order( + p, + self.owner, + if self.tool == Tool::Connect { + None + } else { + self.before.zip(self.after) + }, + ); + if self.tool == Tool::Connect { + let source = self.before.ok_or("Choose the preceding contribution.")?; + let target = self.after.ok_or("Choose the following contribution.")?; + if sequence_links(p, self.owner) + .iter() + .any(|r| r.source() == source && r.target() == target) + { + return Err("These contributions already have a sequence link; select it to edit its guard.".into()); + } + if legacy_order(p, self.owner) + .windows(2) + .any(|pair| pair == [source, target]) + { + return Err("This path already follows the stored order. Convert the ordered path to edit its sequence link.".into()); + } + commands.push(link(p, source, target, &self.guard)); + return Ok(unique_batch(p, commands)); + } + let target = if matches!(self.tool, Tool::Control) { + None + } else { + let id = self + .target + .ok_or("Choose the existing work, exchange or process to involve.")?; + if !targets(p, self.owner, self.tool).iter().any(|c| c.id == id) + || !compatible_source(p, self, id) + { + return Err( + "The selected source is incompatible or would introduce recursive reuse." + .into(), + ); + } + Some(id) + }; + let default_name = target.map(|id| p.named(id)).unwrap_or(match self.control { + ControlKind::Choice => "Choose path", + ControlKind::Merge => "Merge paths", + ControlKind::Fork => "Start parallel paths", + ControlKind::Join => "Join paths", + ControlKind::Loop => "Repeat or exit", + }); + let name = if self.name.trim().is_empty() { + default_name + } else if self.source.label(self.target).is_some() { + self.name.as_str() + } else { + self.name.trim() + }; + let (first, last) = if self.tool == Tool::Branch { + let alternate = self.alternate.ok_or("Choose work for the second path.")?; + if !targets(p, self.owner, Tool::Work) + .iter() + .any(|c| c.id == alternate) + { + return Err("Choose same-layer work for the second path.".into()); + } + let parallel = self.control == ControlKind::Fork; + let split = occurrence( + p, + self.owner, + if parallel { + "Start parallel paths" + } else { + "Choose path" + }, + OccurrenceBody::Control { + kind: if parallel { + ControlKind::Fork + } else { + ControlKind::Choice + }, + }, + ); + let merge = occurrence( + p, + self.owner, + if parallel { + "Join paths" + } else { + "Merge alternatives" + }, + OccurrenceBody::Control { + kind: if parallel { + ControlKind::Join + } else { + ControlKind::Merge + }, + }, + ); + let a = occurrence( + p, + self.owner, + name, + OccurrenceBody::Action { + action: target, + lifeline: None, + }, + ); + let b = occurrence( + p, + self.owner, + p.named(alternate), + OccurrenceBody::Action { + action: Some(alternate), + lifeline: None, + }, + ); + let ids = (split.id, a.id, b.id, merge.id); + commands.extend([split, a, b, merge].into_iter().map(Command::CreateElement)); + commands.extend([ + link(p, ids.0, ids.1, if parallel { "" } else { &self.guard }), + link( + p, + ids.0, + ids.2, + if parallel { "" } else { &self.alternate_guard }, + ), + link(p, ids.1, ids.3, ""), + link(p, ids.2, ids.3, ""), + ]); + (ids.0, ids.3) + } else { + let body = match self.tool { + Tool::Work => OccurrenceBody::Action { + action: target, + lifeline: None, + }, + Tool::Exchange => OccurrenceBody::Exchange { + operation: None, + exchange: target, + sender: None, + receiver: None, + message_kind: MessageKind::default(), + reply_to: None, + values: vec![], + }, + Tool::Reuse => OccurrenceBody::SequenceReference { sequence: target }, + Tool::Control => OccurrenceBody::Control { kind: self.control }, + _ => unreachable!(), + }; + let e = occurrence(p, self.owner, name, body); + let id = e.id; + commands.push(Command::CreateElement(e)); + (id, id) + }; + if let Some(existing) = split_link { + let mut updated = existing.clone(); + if let RelationBody::SequenceLink { target, .. } = &mut updated.body { + *target = first; + } + commands.push(Command::EditRelation(existing.id, updated)); + } else if let Some(before) = self.before { + commands.push(link( + p, + before, + first, + if self.tool == Tool::Branch { + "" + } else { + &self.guard + }, + )); + } + if let Some(after) = self.after { + commands.push(link(p, last, after, "")); + } + let new_ids: Vec<_> = commands + .iter() + .filter_map(|c| match c { + Command::CreateElement(e) if e.parent == Some(self.owner) => Some(e.id), + _ => None, + }) + .collect(); + let mut order = owner.steps().expect("process steps").clone(); + let slot = self.insertion.before.or(self.after); + let index = slot + .map(|id| { + order + .iter() + .position(|v| *v == id) + .ok_or("The presentation slot changed.") + }) + .transpose()? + .unwrap_or(order.len()); + order.splice(index..index, new_ids); + commands.push(Command::SetSteps(self.owner, order)); + Ok(unique_batch(p, commands)) + } +} +pub(super) fn unique_batch(p: &Project, mut commands: Vec) -> Command { + let mut aliases: BTreeSet = p + .identities() + .into_iter() + .map(|(_, alias)| alias.to_owned()) + .collect(); + for command in &mut commands { + let (id, alias) = match command { + Command::CreateElement(e) => (e.id, &mut e.alias), + Command::CreateRelation(r) => (r.id, &mut r.alias), + _ => continue, + }; + if !aliases.insert(alias.clone()) { + *alias = format!("{}_{}", alias, id.simple()); + aliases.insert(alias.clone()); + } + } + Command::Batch(commands) +} +fn event(e: Event) -> Message { + Message::Behavior(if matches!(e, Event::Submit) { + super::Event::Submit + } else { + super::Event::Topology(e) + }) +} +pub fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a, Message> { + view::view(app, owner, out) +} +fn sequence_links(p: &Project, owner: Id) -> Vec<&Relation> { + p.relations + .values() + .filter(|r| { + r.kind() == RelationKind::SequenceLink + && is_contribution(p, owner, r.source()) + && is_contribution(p, owner, r.target()) + }) + .collect() +} +struct GraphNode { + id: Id, + rect: Rectangle, + title: String, + detail: String, + control: Option, + source: Option, + preview: Vec<(OccurrenceEndpoint, String)>, + preview_links: Vec<(OccurrenceEndpoint, OccurrenceEndpoint)>, + preview_total: usize, +} +impl GraphNode { + fn endpoint_rect(&self, endpoint: &OccurrenceEndpoint) -> Rectangle { + self.preview + .iter() + .position(|(candidate, _)| candidate == endpoint) + .map(|i| { + Rectangle::new( + Point::new( + self.rect.x + 12. + (i % 2) as f32 * 125., + self.rect.y + 118. + (i / 2) as f32 * 44., + ), + Size::new(113., 32.), + ) + }) + .unwrap_or(self.rect) + } +} +struct GraphEdge { + id: Id, + source: Id, + target: Id, + guard: String, + points: Vec, + label: Rectangle, + incidence: bool, + layer: EdgeLayer, +} +// Ordered exchange adjacency is a projection, never a persisted relation ID. +struct OrderedPath { + source: Id, + target: Id, + points: Vec, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EdgeLayer { + Direct, + Contextual, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PaintPass { + DirectEdges, + NodeBackgrounds, + ContextualEdges, + NodeContents, + Labels, +} +const PAINT_PASSES: [PaintPass; 5] = [ + PaintPass::DirectEdges, + PaintPass::NodeBackgrounds, + PaintPass::ContextualEdges, + PaintPass::NodeContents, + PaintPass::Labels, +]; +mod contextual_routing; +struct Graph<'a> { + project: &'a Project, + interaction: Option<(Context, CanvasTool, bool)>, + scroll_offset: iced::Vector, + camera_pan: iced::Vector, + pending_insertion: Option, + nodes: Vec, + edges: Vec, + ordered_paths: Vec, + ordered: bool, + size: Size, + selected: Id, + scale: f32, +} +impl<'a> Graph<'a> { + fn new(p: &'a Project, owner: Id, selected: Id) -> Self { + Self::expanded(p, owner, selected, &BTreeSet::new()) + } + fn expanded(p: &'a Project, owner: Id, selected: Id, expanded: &BTreeSet) -> Self { + Self::expanded_qualified(p, owner, selected, expanded, &BTreeSet::new()) + } + fn expanded_qualified( + p: &'a Project, + owner: Id, + selected: Id, + expanded: &BTreeSet, + expanded_paths: &BTreeSet, + ) -> Self { + let context_index = oa_core::topology_context::TopologyIndex::new(p); + let choices = contributions(p, owner); + let links = sequence_links(p, owner); + let order = legacy_order(p, owner); + let adjacency: Vec<_> = links + .iter() + .map(|r| (r.source(), r.target())) + .chain(order.windows(2).map(|pair| (pair[0], pair[1]))) + .collect(); + let ids: BTreeSet<_> = choices.iter().map(|c| c.id).collect(); + let mut incoming: BTreeMap<_, usize> = ids.iter().map(|id| (*id, 0)).collect(); + let mut ranks: BTreeMap<_, usize> = ids.iter().map(|id| (*id, 0)).collect(); + for &(_, target) in &adjacency { + *incoming.entry(target).or_default() += 1; + } + let mut queue: Vec<_> = choices + .iter() + .filter(|c| incoming[&c.id] == 0) + .map(|c| c.id) + .collect(); + let mut done = BTreeSet::new(); + let mut offset = 0; + while offset < queue.len() { + let id = queue[offset]; + offset += 1; + if !done.insert(id) { + continue; + } + for &(_, next) in adjacency.iter().filter(|(source, _)| *source == id) { + ranks.insert(next, ranks[&next].max(ranks[&id] + 1)); + let count = incoming.get_mut(&next).expect("Graph endpoint"); + *count = count.saturating_sub(1); + if *count == 0 { + queue.push(next); + } + } + } + // Cycles remain explicit edges; deterministic fallback placement must + // terminate even while users are building a loop or incomplete graph. + let mut fallback = ranks.values().copied().max().unwrap_or(0) + 1; + for c in &choices { + if !done.contains(&c.id) { + ranks.insert(c.id, fallback); + fallback += 1; + } + } + let mut rows: BTreeMap = BTreeMap::new(); + let mut nodes = vec![]; + let mut size: Size = Size::new(640., 220.); + for c in choices { + let e = &p.elements[&c.id]; + let rank = ranks[&c.id]; + let slot = rows.entry(rank).or_insert(24.); + let mut node = GraphNode { + id: e.id, + rect: Rectangle::new( + Point::new(24. + rank as f32 * 420., *slot), + Size::new(270., 104.), + ), + title: e.name.clone(), + detail: String::new(), + control: None, + source: None, + preview: vec![], + preview_links: vec![], + preview_total: 0, + }; + if let Some(SharedBody::Occurrence { body, .. }) = e.shared() { + match body { + OccurrenceBody::Action { action, .. } => { + node.detail = action + .map(|id| format!("Work · {}", p.named(id))) + .unwrap_or("Choose work in Properties".into()) + } + OccurrenceBody::Exchange { exchange, .. } => { + node.detail = exchange + .and_then(|id| p.relations.get(&id)) + .map(|r| { + format!( + "Exchange · {} → {}", + p.named(r.source()), + p.named(r.target()) + ) + }) + .unwrap_or("Choose exchange in Properties".into()) + } + OccurrenceBody::Control { kind } => { + node.control = Some(*kind); + node.detail = ControlChoice(*kind).to_string(); + } + OccurrenceBody::SequenceReference { sequence } => { + node.source = *sequence; + node.rect.height = 218.; + node.detail = sequence + .map(|id| format!("ref · {}", p.named(id))) + .unwrap_or("Choose source in Properties".into()); + if let Some(source) = sequence { + let contributions = contributions(p, *source); + node.preview_total = contributions.len(); + node.preview = contextual::preview( + &context_index, + p, + owner, + e.id, + expanded_paths, + if expanded.contains(&e.id) { 512 } else { 4 }, + ) + .into_iter() + .map(|endpoint| { + let name = format!( + "{}{}", + "› ".repeat(endpoint.references.len().saturating_sub(1).min(3)), + p.named(endpoint.occurrence) + ); + (endpoint, name) + }) + .collect(); + node.rect.height = + (132. + node.preview.len().div_ceil(2) as f32 * 44.).max(218.); + let preview_ids: BTreeSet<_> = + node.preview.iter().map(|(id, _)| id.clone()).collect(); + node.preview_links = sequence_links(p, *source) + .into_iter() + .filter(|r| { + preview_ids.contains(&OccurrenceEndpoint { + references: vec![e.id], + occurrence: r.source(), + }) && preview_ids.contains(&OccurrenceEndpoint { + references: vec![e.id], + occurrence: r.target(), + }) + }) + .map(|r| { + ( + OccurrenceEndpoint { + references: vec![e.id], + occurrence: r.source(), + }, + OccurrenceEndpoint { + references: vec![e.id], + occurrence: r.target(), + }, + ) + }) + .collect(); + } + } + _ => {} + } + } + *slot += node.rect.height + 36.; + size.width = size.width.max(node.rect.x + node.rect.width + 28.); + size.height = size.height.max(node.rect.y + node.rect.height + 42.); + nodes.push(node); + } + let ordered_paths = order + .windows(2) + .map(|pair| { + let a = nodes + .iter() + .find(|n| n.id == pair[0]) + .expect("ordered source") + .rect; + let b = nodes + .iter() + .find(|n| n.id == pair[1]) + .expect("ordered target") + .rect; + OrderedPath { + source: pair[0], + target: pair[1], + points: vec![ + Point::new(a.x + a.width, a.center_y()), + Point::new(b.x, b.center_y()), + ], + } + }) + .collect(); + let mut edges = vec![]; + let mut routed_pairs = BTreeSet::new(); + for (i, r) in links.into_iter().enumerate() { + let a = nodes + .iter() + .find(|n| n.id == r.source()) + .expect("Graph source") + .rect; + let b = nodes + .iter() + .find(|n| n.id == r.target()) + .expect("Graph target") + .rect; + let start = Point::new(a.x + a.width, a.center_y()); + let end = Point::new(b.x, b.center_y()); + // Parallel records keep distinct visible paths and guard hit targets. + // Their semantic endpoints and IDs remain unchanged. + let forward = end.x > start.x && routed_pairs.insert((r.source(), r.target())); + let points = if forward { + let mid = (start.x + end.x) / 2.; + vec![start, Point::new(mid, start.y), Point::new(mid, end.y), end] + } else { + let below = size.height + 24. + i as f32 * 42.; + vec![ + start, + Point::new(start.x + 18., start.y), + Point::new(start.x + 18., below), + Point::new(end.x - 16., below), + Point::new(end.x - 16., end.y), + end, + ] + }; + let guard = match &r.body { + RelationBody::SequenceLink { guard, .. } => guard.clone(), + _ => String::new(), + }; + let label_at = if forward { + Point::new(end.x - 138., end.y - 43.) + } else { + Point::new(((start.x + end.x) / 2. - 66.).max(4.), points[2].y - 38.) + }; + let label = Rectangle::new(label_at, Size::new(132., 36.)); + edges.push(GraphEdge { + id: r.id, + source: r.source(), + target: r.target(), + guard, + points, + label, + incidence: false, + layer: EdgeLayer::Direct, + }); + } + let mut contextual_pairs = BTreeSet::new(); + for r in p.relations.values() { + let RelationBody::ContextualLink { + owner: caller, + source, + target, + purpose, + } = &r.body + else { + continue; + }; + if *caller != owner { + continue; + } + let source_id = source + .references + .first() + .copied() + .unwrap_or(source.occurrence); + let target_id = target + .references + .first() + .copied() + .unwrap_or(target.occurrence); + let Some(a) = nodes + .iter() + .find(|n| n.id == source_id) + .map(|n| contextual_routing::anchor(n, source, false)) + else { + continue; + }; + let Some(b) = nodes + .iter() + .find(|n| n.id == target_id) + .map(|n| contextual_routing::anchor(n, target, true)) + else { + continue; + }; + let below = size.height + 30. + edges.len() as f32 * 48.; + let first = contextual_pairs.insert((source.clone(), target.clone())); + let (points, label) = contextual_routing::route(a, b, below, first); + let (caption, incidence) = match purpose { + ContextualLinkPurpose::Precedence { guard } => { + (format!("Precedence · {guard}"), false) + } + ContextualLinkPurpose::Exchange { exchange } => { + (format!("Assembly · {}", p.named(*exchange)), true) + } + ContextualLinkPurpose::CommonWork { work } => { + (format!("Common work · {}", p.named(*work)), true) + } + }; + edges.push(GraphEdge { + id: r.id, + source: source_id, + target: target_id, + guard: caption, + points, + label, + incidence, + layer: EdgeLayer::Contextual, + }); + } + for e in &edges { + for pt in &e.points { + size.height = size.height.max(pt.y + 30.); + if e.layer == EdgeLayer::Contextual { + size.width = size.width.max(pt.x + 24.); + } + } + if e.layer == EdgeLayer::Contextual { + size.width = size.width.max(e.label.x + e.label.width + 24.); + size.height = size.height.max(e.label.y + e.label.height + 24.); + } + } + Self { + project: p, + interaction: None, + scroll_offset: iced::Vector::ZERO, + camera_pan: iced::Vector::ZERO, + pending_insertion: None, + nodes, + edges, + ordered_paths, + ordered: !order.is_empty(), + size, + selected, + scale: 1., + } + } +} +fn wrapped(value: &str, width: usize, lines: usize) -> String { + let mut output = Vec::new(); + let mut current = String::new(); + let words: Vec = value + .split_whitespace() + .flat_map(|word| { + word.chars() + .collect::>() + .chunks(width.max(1)) + .map(|chunk| chunk.iter().collect::()) + .collect::>() + }) + .collect(); + for word in &words { + if !current.is_empty() && current.chars().count() + word.chars().count() + 1 > width { + output.push(current); + current = String::new(); + } + if !current.is_empty() { + current.push(' '); + } + current.push_str(word); + } + if !current.is_empty() { + output.push(current); + } + let more = output.len() > lines; + output.truncate(lines); + if more && let Some(last) = output.last_mut() { + last.push('…'); + } + output.join("\n") +} +fn draw_text(f: &mut Frame, content: String, position: Point, size: f32, color: Color) { + f.fill_text(Text { + content, + position, + size: size.into(), + color, + ..Default::default() + }); +} +fn draw_arrow(f: &mut Frame, points: &[Point], color: Color, width: f32) { + if points.len() < 2 { + return; + } + f.stroke( + &Path::new(|p| { + p.move_to(points[0]); + for pt in &points[1..] { + p.line_to(*pt); + } + }), + Stroke::default().with_color(color).with_width(width), + ); + let end = points[points.len() - 1]; + let delta = end - points[points.len() - 2]; + let length = (delta.x * delta.x + delta.y * delta.y).sqrt().max(1.); + let d = delta / length; + f.stroke( + &Path::new(|p| { + p.move_to(end - d * 9. + iced::Vector::new(-d.y, d.x) * 4.); + p.line_to(end); + p.line_to(end - d * 9. - iced::Vector::new(-d.y, d.x) * 4.); + }), + Stroke::default().with_color(color).with_width(width), + ); +} +impl canvas::Program for Graph<'_> { + type State = Input; + fn update( + &self, + state: &mut Input, + event: &canvas::Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + self.input(state, event, bounds, cursor) + } + fn draw( + &self, + state: &Input, + renderer: &Renderer, + _: &Theme, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Vec { + let mut f = Frame::new(renderer, bounds.size()); + f.fill_rectangle(Point::ORIGIN, bounds.size(), Color::WHITE); + f.translate(self.camera_pan); + f.scale(self.scale); + for pass in PAINT_PASSES { + match pass { + PaintPass::DirectEdges | PaintPass::ContextualEdges => { + if pass == PaintPass::DirectEdges { + for path in &self.ordered_paths { + let selected = + path.source == self.selected || path.target == self.selected; + draw_arrow( + &mut f, + &path.points, + if selected { style::TEAL } else { style::MUTED }, + if selected { 2.5 } else { 1.5 }, + ); + } + } + for e in self.edges.iter().filter(|edge| { + edge.layer + == if pass == PaintPass::DirectEdges { + EdgeLayer::Direct + } else { + EdgeLayer::Contextual + } + }) { + let selected = e.id == self.selected + || e.source == self.selected + || e.target == self.selected; + if e.incidence { + f.stroke( + &Path::new(|path| { + path.move_to(e.points[0]); + for point in &e.points[1..] { + path.line_to(*point); + } + }), + Stroke::default() + .with_color(if selected { style::TEAL } else { style::MUTED }) + .with_width(if selected { 2.5 } else { 1.5 }), + ); + } else { + draw_arrow( + &mut f, + &e.points, + if selected { style::TEAL } else { style::MUTED }, + if selected { 2.5 } else { 1.5 }, + ); + } + } + } + PaintPass::NodeBackgrounds => { + for n in &self.nodes { + let rect = + Path::rounded_rectangle(n.rect.position(), n.rect.size(), 10.into()); + f.fill(&rect, Color::from_rgb8(243, 248, 248)); + f.stroke( + &rect, + Stroke::default() + .with_color(if n.id == self.selected { + style::TEAL + } else { + style::LINE + }) + .with_width(if n.id == self.selected { 2.5 } else { 1. }), + ); + } + } + PaintPass::NodeContents => { + for n in &self.nodes { + let text_x = n.rect.x + if n.control.is_some() { 68. } else { 12. }; + if let Some(kind) = n.control { + let center = Point::new(n.rect.x + 33., n.rect.y + 34.); + let glyph = match kind { + ControlKind::Choice | ControlKind::Merge => Path::new(|p| { + p.move_to(Point::new(center.x, center.y - 22.)); + p.line_to(Point::new(center.x + 23., center.y)); + p.line_to(Point::new(center.x, center.y + 22.)); + p.line_to(Point::new(center.x - 23., center.y)); + p.close(); + }), + ControlKind::Fork | ControlKind::Join => Path::rectangle( + Point::new(center.x - 23., center.y - 5.), + Size::new(46., 10.), + ), + ControlKind::Loop => Path::circle(center, 23.), + }; + f.stroke( + &glyph, + Stroke::default().with_color(style::TEAL).with_width(2.), + ); + if matches!(kind, ControlKind::Fork | ControlKind::Join) { + f.fill(&glyph, style::TEAL); + } else { + draw_text( + &mut f, + if kind == ControlKind::Loop { + "IT" + } else { + "OR" + } + .into(), + Point::new(center.x - 10., center.y - 8.), + 12., + style::TEAL, + ); + } + } + draw_text( + &mut f, + wrapped(&n.title, if n.control.is_some() { 25 } else { 34 }, 2), + Point::new(text_x, n.rect.y + 12.), + 13., + style::INK, + ); + draw_text( + &mut f, + wrapped(&n.detail, 37, 2), + Point::new(n.rect.x + 12., n.rect.y + 60.), + 11., + style::MUTED, + ); + if n.source.is_some() { + draw_text( + &mut f, + format!( + "{} shown · {} direct contributions", + n.preview.len(), + n.preview_total + ), + Point::new(n.rect.x + 12., n.rect.y + 94.), + 11., + style::TEAL, + ); + let mini: BTreeMap<_, _> = n + .preview + .iter() + .enumerate() + .map(|(i, (id, _))| { + ( + id.clone(), + Rectangle::new( + Point::new( + n.rect.x + 12. + (i % 2) as f32 * 125., + n.rect.y + 118. + (i / 2) as f32 * 44., + ), + Size::new(113., 32.), + ), + ) + }) + .collect(); + for (a, b) in &n.preview_links { + let a = mini[a]; + let b = mini[b]; + draw_arrow( + &mut f, + &[ + Point::new(a.x + a.width, a.center_y()), + Point::new(b.x, b.center_y()), + ], + style::MUTED, + 1., + ); + } + for (id, name) in &n.preview { + let r = mini[id]; + f.fill_rectangle(r.position(), r.size(), Color::WHITE); + f.stroke( + &Path::rectangle(r.position(), r.size()), + Stroke::default().with_color(style::LINE), + ); + draw_text( + &mut f, + wrapped(name, 17, 2), + Point::new(r.x + 4., r.y + 3.), + 9., + style::INK, + ); + } + if n.preview.is_empty() { + draw_text( + &mut f, + "Source has no contributions yet".into(), + Point::new(n.rect.x + 12., n.rect.y + 136.), + 11., + style::MUTED, + ); + } + } + } + } + PaintPass::Labels => { + for e in &self.edges { + if !e.guard.is_empty() { + f.fill_rectangle(e.label.position(), e.label.size(), Color::WHITE); + draw_text( + &mut f, + wrapped(&format!("[{}]", e.guard), 19, 2), + e.label.position(), + 11., + style::INK, + ); + } + } + } + } + } + self.overlay(state, &mut f, cursor, bounds); + vec![f.into_geometry()] + } + fn mouse_interaction( + &self, + _: &Input, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> mouse::Interaction { + if cursor.position_in(bounds).is_some_and(|p| { + let p = Point::new( + (p.x - self.camera_pan.x) / self.scale, + (p.y - self.camera_pan.y) / self.scale, + ); + self.nodes.iter().any(|n| n.rect.contains(p)) + || self.ordered_paths.iter().any(|path| { + path.points.windows(2).any(|pair| { + crate::diagram::distance_to_segment(p, pair[0], pair[1]) < 8. / self.scale + }) + }) + || self.edges.iter().any(|e| { + e.label.contains(p) + || e.points + .windows(2) + .any(|v| crate::diagram::distance_to_segment(p, v[0], v[1]) < 8.) + }) + }) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fixture() -> (Project, Id, Id, Id, Id, Id) { + let mut p = Project::demo(); + let owner = p + .make_element(Kind::Process, Layer::Oa, "Branch test", None) + .unwrap(); + let owner_id = owner.id; + p = execute_batch(&p, &[Command::CreateElement(owner)]).unwrap(); + let a = p + .make_element(Kind::Activity, Layer::Oa, "Shared display name", None) + .unwrap(); + let a_id = a.id; + p = execute_batch(&p, &[Command::CreateElement(a)]).unwrap(); + let b = p + .make_element(Kind::Activity, Layer::Oa, "Shared display name", None) + .unwrap(); + let b_id = b.id; + p = execute_batch(&p, &[Command::CreateElement(b)]).unwrap(); + let before = occurrence( + &p, + owner_id, + "Start work", + OccurrenceBody::Action { + action: Some(a_id), + lifeline: None, + }, + ); + let after = occurrence( + &p, + owner_id, + "Finish work", + OccurrenceBody::Action { + action: Some(b_id), + lifeline: None, + }, + ); + let before_id = before.id; + let after_id = after.id; + p = execute_batch( + &p, + &[ + Command::CreateElement(before), + Command::CreateElement(after), + ], + ) + .unwrap(); + (p, owner_id, a_id, b_id, before_id, after_id) + } + fn branch(owner: Id, a: Id, b: Id, before: Id, after: Id) -> Draft { + Draft { + source: Default::default(), + invocation: Id::new_v4(), + source_origin: None, + insertion: Insertion::default(), + fixed: false, + browsers: Default::default(), + owner, + tool: Tool::Branch, + name: String::new(), + target: Some(a), + alternate: Some(b), + before: Some(before), + after: Some(after), + guard: "request accepted".into(), + alternate_guard: "else".into(), + control: ControlKind::Choice, + contextual: None, + } + } + fn legacy_fixture() -> (Project, Id) { + let mut p = Project::demo(); + let owner = p + .make_element(Kind::Process, Layer::Oa, "Existing ordered path", None) + .unwrap(); + let owner_id = owner.id; + p = execute_batch(&p, &[Command::CreateElement(owner)]).unwrap(); + let mut work = vec![]; + for index in 0..4 { + let e = p + .make_element( + Kind::Activity, + Layer::Oa, + &format!("Ordered work {index}"), + None, + ) + .unwrap(); + work.push(e.id); + p = execute_batch(&p, &[Command::CreateElement(e)]).unwrap(); + } + let mut exchanges = vec![]; + for (index, pair) in work.windows(2).enumerate() { + let id = Id::new_v4(); + let name = format!("Ordered exchange {index}"); + let r = Relation { + id, + alias: p.fresh_alias(&name, "relation", id), + name, + description: String::new(), + body: RelationBody::new(RelationKind::Interaction, pair[0], pair[1]), + }; + p = execute_batch(&p, &[Command::CreateRelation(r)]).unwrap(); + exchanges.push(id); + } + let commands = (0..3) + .map(|index| { + Command::CreateElement(occurrence( + &p, + owner_id, + &format!("Ordered contribution {index}"), + OccurrenceBody::Exchange { + operation: None, + exchange: Some(exchanges[index % exchanges.len()]), + sender: None, + receiver: None, + message_kind: MessageKind::default(), + reply_to: None, + values: vec![], + }, + )) + }) + .collect(); + p = execute_batch(&p, &[unique_batch(&p, commands)]).unwrap(); + (p, owner_id) + } + #[test] + fn empty_process_can_start_through_normal_authoring_events() { + let mut p = Project::demo(); + let owner = p + .make_element(Kind::Process, Layer::Oa, "New operational process", None) + .unwrap(); + let owner_id = owner.id; + p = execute_batch(&p, &[Command::CreateElement(owner)]).unwrap(); + let source = targets(&p, owner_id, Tool::Work).first().unwrap().clone(); + let mut state = State::default(); + state + .update(&p, Event::Start(owner_id, Tool::Work, None)) + .unwrap(); + state.update(&p, Event::Target(source.clone())).unwrap(); + state + .update(&p, Event::Name("First contribution".into())) + .unwrap(); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + let result = execute_batch(&p, &[command]).unwrap(); + let steps = result.elements[&owner_id].steps().unwrap(); + assert_eq!(steps.len(), 1); + assert!(uses_explicit_topology(&result, owner_id)); + assert!( + matches!(result.elements[&steps[0]].shared(), Some(SharedBody::Occurrence { + body: OccurrenceBody::Action { action: Some(id), .. }, .. }) if *id == source.id) + ); + assert_eq!(Graph::new(&result, owner_id, owner_id).nodes.len(), 1); + } + #[test] + fn legacy_edit_preserves_order_and_new_work_in_one_undo() { + let (p, owner) = legacy_fixture(); + let original_order = legacy_order(&p, owner); + let source = targets(&p, owner, Tool::Work).first().unwrap().clone(); + let mut state = State::default(); + state + .update(&p, Event::Start(owner, Tool::Work, None)) + .unwrap(); + state.update(&p, Event::Target(source)).unwrap(); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + let mut session = Session::new(p.clone()); + session.execute(command).unwrap(); + let links = sequence_links(session.project(), owner); + for pair in original_order.windows(2) { + assert!( + links + .iter() + .any(|r| r.source() == pair[0] && r.target() == pair[1]) + ); + } + assert_eq!( + session.project().elements[&owner].steps().unwrap().len(), + original_order.len() + 1 + ); + assert!(uses_explicit_topology(session.project(), owner)); + session.undo(); + assert_eq!(session.project(), &p); + } + #[test] + fn legacy_branch_insertion_replaces_only_the_selected_implied_link() { + let (p, owner) = legacy_fixture(); + let order = legacy_order(&p, owner); + let sources = targets(&p, owner, Tool::Work); + let mut state = State::default(); + state + .update(&p, Event::Start(owner, Tool::Branch, Some(order[0]))) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().after, Some(order[1])); + state.update(&p, Event::Target(sources[0].clone())).unwrap(); + state + .update(&p, Event::Alternate(sources[1].clone())) + .unwrap(); + state.update(&p, Event::Guard("ready".into())).unwrap(); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + let result = execute_batch(&p, &[command]).unwrap(); + let links = sequence_links(&result, owner); + assert!( + !links + .iter() + .any(|r| r.source() == order[0] && r.target() == order[1]) + ); + for pair in order.windows(2).skip(1) { + assert!( + links + .iter() + .any(|r| r.source() == pair[0] && r.target() == pair[1]) + ); + } + assert_eq!(links.len(), order.len() - 2 + 6); + assert_eq!( + result.elements[&owner].steps().unwrap().len(), + order.len() + 4 + ); + } + #[test] + fn insertion_picker_only_offers_work_matching_adjacent_exchanges() { + let (p, owner) = legacy_fixture(); + let order = legacy_order(&p, owner); + let expected = contribution_exchange(&p, order[0]).unwrap().target(); + let mut state = State::default(); + state + .update(&p, Event::Start(owner, Tool::Work, Some(order[0]))) + .unwrap(); + let d = state.draft.as_ref().unwrap(); + let choices: Vec<_> = targets(&p, owner, Tool::Work) + .into_iter() + .filter(|choice| compatible_source(&p, d, choice.id)) + .collect(); + assert_eq!(choices.len(), 1); + assert_eq!(choices[0].id, expected); + state.update(&p, Event::Target(choices[0].clone())).unwrap(); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + assert!(execute_batch(&p, &[command]).is_ok()); + } + #[test] + fn guarded_branch_is_one_transaction_with_distinct_source_identities() { + let (mut p, owner, a, b, before, after) = fixture(); + p = execute_batch(&p, &[link(&p, before, after, "")]).unwrap(); + let old_link = sequence_links(&p, owner)[0].id; + let mut session = Session::new(p.clone()); + let command = branch(owner, a, b, before, after).command(&p).unwrap(); + session.execute(command).unwrap(); + let result = session.project(); + assert!(result.relations.contains_key(&old_link)); + assert_eq!(result.elements[&owner].steps().unwrap().len(), 6); + assert_eq!(sequence_links(result, owner).len(), 6); + assert_eq!( + result + .elements + .values() + .filter(|e| e.kind() == Kind::Activity) + .count(), + p.elements + .values() + .filter(|e| e.kind() == Kind::Activity) + .count() + ); + let involved: Vec<_> = result + .children(owner) + .filter(|e| !p.elements.contains_key(&e.id)) + .filter_map(|e| match e.shared() { + Some(SharedBody::Occurrence { + body: OccurrenceBody::Action { action, .. }, + .. + }) => *action, + _ => None, + }) + .collect(); + assert!(involved.contains(&a) && involved.contains(&b)); + let graph = Graph::new(result, owner, owner); + assert_eq!(graph.edges.len(), 6); + assert!(graph.edges.iter().any(|e| e.guard == "request accepted")); + assert!(graph.edges.iter().any(|e| e.guard == "else")); + for edge in graph.edges.iter().filter(|e| !e.guard.is_empty()) { + assert!( + !graph + .nodes + .iter() + .any(|node| node.rect.contains(edge.label.center())), + "Guard must not cover a contribution" + ); + } + let issues = validation::validate(result); + assert!(!issues.iter().any(|i| { + matches!( + i.code.as_str(), + "control_cardinality" | "ambiguous_sequence_branch" | "missing_sequence_guard" + ) && i.element.is_some_and(|id| { + id == owner + || result + .elements + .get(&id) + .is_some_and(|e| e.parent == Some(owner)) + }) + })); + session.undo(); + assert_eq!(session.project(), &p); + } + #[test] + fn unlinked_contributions_do_not_gain_fictional_edges() { + let (p, owner, ..) = fixture(); + let graph = Graph::new(&p, owner, owner); + assert_eq!(graph.nodes.len(), 2); + assert!(graph.edges.is_empty()); + } + #[test] + fn reusable_preview_preserves_identity_and_filters_recursive_sources() { + let (mut p, owner, a, _, before, after) = fixture(); + let source = p + .make_element(Kind::Process, Layer::Oa, "Reusable source", None) + .unwrap(); + let source_id = source.id; + p = execute_batch(&p, &[Command::CreateElement(source)]).unwrap(); + let inside = occurrence( + &p, + source_id, + "Existing contribution", + OccurrenceBody::Action { + action: Some(a), + lifeline: None, + }, + ); + let inside_id = inside.id; + p = execute_batch(&p, &[Command::CreateElement(inside)]).unwrap(); + let mut d = branch(owner, a, a, before, after); + d.tool = Tool::Reuse; + d.target = Some(source_id); + let result = execute_batch(&p, &[d.command(&p).unwrap()]).unwrap(); + let graph = Graph::new(&result, owner, owner); + let reference = graph + .nodes + .iter() + .find(|n| n.source == Some(source_id)) + .unwrap(); + assert_eq!( + reference.preview[0].0, + OccurrenceEndpoint { + references: vec![reference.id], + occurrence: inside_id + } + ); + assert_eq!(result.children(source_id).count(), 1); + assert!( + !targets(&result, source_id, Tool::Reuse) + .iter() + .any(|c| c.id == owner) + ); + assert!( + !targets(&result, owner, Tool::Reuse) + .iter() + .any(|c| c.id == owner) + ); + } + #[test] + fn stale_or_invalid_submit_preserves_inputs_and_creates_nothing() { + let (p, owner, a, b, before, after) = fixture(); + let mut state = State { + draft: Some(branch(owner, a, b, before, after)), + ..State::default() + }; + state + .update( + &p, + Event::Target(Choice { + id: Id::nil(), + label: "Missing".into(), + }), + ) + .unwrap(); + assert!(state.update(&p, Event::Submit).is_err()); + assert!(state.draft.is_some()); + state + .update( + &p, + Event::Target(Choice { + id: a, + label: "Work".into(), + }), + ) + .unwrap(); + let command = state.update(&p, Event::Submit).unwrap().unwrap(); + assert!(state.draft.is_some()); + assert!(matches!(command, Command::Batch(_))); + assert_eq!(p.elements[&owner].steps().unwrap().len(), 2); + let result = execute_batch(&p, &[command]).unwrap(); + state.acknowledge_commit(); + assert!(state.draft.is_none()); + assert_eq!(result.elements[&owner].steps().unwrap().len(), 6); + } + #[test] + fn cycle_projection_terminates_and_retains_actual_links() { + let (mut p, owner, _, _, before, after) = fixture(); + for command in [ + link(&p, before, after, "continue"), + link(&p, after, before, "retry"), + ] { + if let Command::CreateRelation(mut r) = command { + r.alias = format!("cycle_{}", r.id.simple()); + p.relations.insert(r.id, r); + } + } + let graph = Graph::new(&p, owner, owner); + assert_eq!(graph.edges.len(), 2); + assert!(graph.size.width.is_finite() && graph.size.height.is_finite()); + assert!(graph.edges.iter().any(|e| e.guard == "retry")); + assert!( + graph + .edges + .iter() + .all(|e| e.label.x >= 0. && e.label.y >= 0.) + ); + } + + #[test] + fn long_engineering_identifiers_stay_inside_label_width() { + let label = wrapped( + "HandlePassengerServiceAvailabilityWhileAircraftIsDecompressing", + 19, + 2, + ); + assert!(label.ends_with('…')); + assert_eq!(label.lines().count(), 2); + assert!(label.lines().all(|line| line.chars().count() <= 20)); + } + + #[test] + fn fitting_a_branch_keeps_all_nodes_visible_and_selection_uses_scaled_coordinates() { + let (p, owner, a, b, before, after) = fixture(); + let p = execute_batch( + &p, + &[branch(owner, a, b, before, after).command(&p).unwrap()], + ) + .unwrap(); + let mut graph = Graph::new(&p, owner, owner); + let mut state = State::default(); + state.viewport(Size::new(486., 460.)); + state.zoom(None); + graph.scale = state.scale(graph.size, 500.); + assert!(graph.size.width * graph.scale <= 462.1); + assert!(graph.size.height * graph.scale <= 450.1); + let node = graph.nodes.last().unwrap(); + let click = Point::new( + (node.rect.x + 20.) * graph.scale, + (node.rect.y + 20.) * graph.scale, + ); + let result = canvas::Program::update( + &graph, + &mut Input::default(), + &canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + Rectangle::new(Point::ORIGIN, Size::new(486., 460.)), + mouse::Cursor::Available(click), + ) + .unwrap() + .into_inner() + .0; + assert!(matches!(result, Some(Message::Select(id)) if id == node.id)); + state.zoom(Some(1.)); + state.viewport(Size::new(800., 460.)); + assert_eq!(state.scale(graph.size, 500.), 1.); + state.zoom(None); + assert!(state.scale(graph.size, 500.) < 1.); + } + + #[test] + fn process_fit_uses_measured_height_and_reports_minimum_without_changing_manual_zoom() { + let mut state = State::default(); + let graph = Size::new(420., 960.); + state.zoom(None); + let mut previous = 0.; + for height in [180., 320., 600.] { + state.viewport(Size::new(1000., height)); + let scale = state.scale(graph, 200.); + assert!(scale > previous); + assert!(graph.height * scale <= height - 10. + 0.01); + assert!(!state.fit_limited(graph, 200.)); + previous = scale; + } + state.viewport(Size::new(500., 180.)); + let huge = Size::new(500., 10_000.); + assert_eq!(state.scale(huge, 200.), 0.05); + assert!(state.fit_limited(huge, 200.)); + state.zoom(Some(1.)); + for height in [180., 600.] { + state.viewport(Size::new(500., height)); + assert_eq!(state.scale(huge, 200.), 1.); + assert!(!state.fit_limited(huge, 200.)); + } + } + + #[test] + fn process_fit_disclosures_and_focus_preserve_rejected_raw_draft_and_accepted_source() { + let (p, owner, _, _, _, _) = fixture(); + let mut app = App::with_project(p); + let _ = app.update(Message::Select(owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + for input in [ + Event::Start(owner, Tool::Contextual, None), + Event::Name(" pending 雨 ".into()), + Event::Contextual(contextual::Event::Guard(" unavailable && 雨 ".into())), + ] { + let _ = app.update(event(input)); + } + let before = app.behavior.recovery_snapshot().unwrap(); + let accepted = app.document.snapshot(); + assert!(app.behavior.prepare_pending(app.project()).is_err()); + for (shelf, expected) in [ + (Shelf::Tools, Some(Shelf::Tools)), + (Shelf::Selection, Some(Shelf::Selection)), + (Shelf::Selection, None), + ] { + let focused = app.behavior.topology.focused; + let _ = app.update(event(Event::Shelf(shelf))); + assert_eq!(app.behavior.topology.shelf, expected); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Focus(context))); + assert_eq!(app.behavior.topology.focused, !focused); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), before); + assert!(app.behavior.prepare_pending(app.project()).is_err()); + assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &accepted)); + } + } + + #[test] + fn process_fit_canvas_selection_ignores_retained_ordered_page_scroll() { + let (p, owner, _, _, _, _) = fixture(); + let mut state = State::default(); + state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(11., 19.)); + state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 370.)); + let context = Context { + document: p.id, + owner, + revision: 0, + epoch: 0, + }; + for scale in [0.45, 1.] { + let mut graph = Graph::new(&p, owner, owner); + graph.scale = scale; + graph.scroll_offset = state.canvas_offset(); + graph.interaction = Some((context, CanvasTool::Select, true)); + let node = &graph.nodes[0]; + let id = node.id; + let point = node.rect.center(); + let bounds = Rectangle::new(Point::new(40., 100.), Size::new(800., 300.)); + let screen = Point::new( + bounds.x + point.x * scale - 11., + bounds.y + point.y * scale - 19., + ); + let message = graph + .input( + &mut Input::default(), + &canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + bounds, + mouse::Cursor::Available(screen), + ) + .and_then(|a| a.into_inner().0); + assert!( + matches!(message,Some(Message::TopologyCanvas(CanvasEvent::Select(c,actual))) if c==context && actual==id) + ); + } + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/contextual.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/contextual.rs new file mode 100644 index 00000000..7c178649 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/contextual.rs @@ -0,0 +1,791 @@ +//! Lazy qualified authoring over the core address resolver. A folded reference +//! never replaces its retained endpoint with the reference or a same-named leaf. +use super::*; +use iced::widget::column; +use oa_core::topology_context::TopologyIndex; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)] +pub enum Purpose { + #[default] + Precedence, + Exchange, + CommonWork, +} +impl std::fmt::Display for Purpose { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Precedence => "Precedence", + Self::Exchange => "Exchange assembly", + Self::CommonWork => "Common work incidence", + }) + } +} +#[derive(Debug, Clone)] +pub enum Event { + Side(bool), + Pick(OccurrenceEndpoint), + Expand(OccurrenceEndpoint), + Up, + Root, + Purpose(Purpose), + Evidence(Choice), + Guard(String), + Search(String), + Page(usize), +} +#[derive(Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct Draft { + edit: Option, + source: Option, + target: Option, + target_side: bool, + purpose: Purpose, + evidence: Option, + guard: String, + search: String, + page: usize, + #[serde(default)] + scope: Vec, +} +impl Draft { + pub(super) fn seed_source(&mut self, endpoint: OccurrenceEndpoint) { + if self.edit.is_none() { + self.source = Some(endpoint); + } + } + pub(super) fn new(p: &Project, owner: Id, selected: Option) -> Self { + if let Some(r) = selected.and_then(|id| p.relations.get(&id)) + && let RelationBody::ContextualLink { + owner: caller, + source, + target, + purpose, + } = &r.body + && *caller == owner + { + let (purpose, evidence, guard) = match purpose { + ContextualLinkPurpose::Precedence { guard } => { + (Purpose::Precedence, None, guard.clone()) + } + ContextualLinkPurpose::Exchange { exchange } => { + (Purpose::Exchange, Some(*exchange), String::new()) + } + ContextualLinkPurpose::CommonWork { work } => { + (Purpose::CommonWork, Some(*work), String::new()) + } + }; + return Self { + edit: Some(r.id), + source: Some(source.clone()), + target: Some(target.clone()), + purpose, + evidence, + guard, + ..Self::default() + }; + } + Self { + source: selected + .filter(|id| is_contribution(p, owner, *id)) + .map(|occurrence| OccurrenceEndpoint { + references: vec![], + occurrence, + }), + ..Self::default() + } + } + pub(super) fn update(&mut self, event: Event) { + match event { + Event::Side(target) => self.target_side = target, + Event::Pick(endpoint) => { + if self.target_side { + self.target = Some(endpoint); + } else { + self.source = Some(endpoint); + } + } + Event::Expand(endpoint) => { + self.scope = endpoint.references; + self.scope.push(endpoint.occurrence); + self.search.clear(); + self.page = 0; + } + Event::Up => { + self.scope.pop(); + self.search.clear(); + self.page = 0; + } + Event::Root => { + self.scope.clear(); + self.search.clear(); + self.page = 0; + } + Event::Purpose(purpose) => self.purpose = purpose, + Event::Evidence(choice) => self.evidence = Some(choice.id), + Event::Guard(raw) => self.guard = raw, + Event::Search(raw) => { + self.search = raw; + self.page = 0; + } + Event::Page(page) => self.page = page, + } + } + fn purpose(&self) -> Result { + Ok(match self.purpose { + Purpose::Precedence => ContextualLinkPurpose::Precedence { + guard: self.guard.clone(), + }, + Purpose::Exchange => ContextualLinkPurpose::Exchange { + exchange: self + .evidence + .ok_or("Choose the exact exchange used for assembly.")?, + }, + Purpose::CommonWork => ContextualLinkPurpose::CommonWork { + work: self.evidence.ok_or("Choose the shared work identity.")?, + }, + }) + } + pub(super) fn command(&self, p: &Project, owner: Id, name: &str) -> Result { + let source = self + .source + .as_ref() + .ok_or("Choose the qualified source contribution.")?; + let target = self + .target + .as_ref() + .ok_or("Choose the qualified target contribution.")?; + let purpose = self.purpose()?; + TopologyIndex::new(p) + .validate_link(owner, source, target, &purpose) + .map_err(|e| e.to_string())?; + let mut relation = if let Some(id) = self.edit { + p.relations.get(&id).filter(|r| matches!(&r.body, RelationBody::ContextualLink { owner: caller, .. } if *caller==owner)).ok_or("This caller-owned link no longer exists.")?.clone() + } else { + let id = Id::new_v4(); + Relation { + id, + alias: p.fresh_alias("contextual_link", "relation", id), + name: self.purpose.to_string(), + description: String::new(), + body: RelationBody::ContextualLink { + owner, + source: source.clone(), + target: target.clone(), + purpose: purpose.clone(), + }, + } + }; + if !name.trim().is_empty() { + relation.name = name.to_owned(); + } + relation.body = RelationBody::ContextualLink { + owner, + source: source.clone(), + target: target.clone(), + purpose, + }; + let command = if self.edit.is_some() { + Command::EditRelation(relation.id, relation) + } else { + Command::CreateRelation(relation) + }; + execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?; + Ok(command) + } +} + +pub(crate) fn address_label(p: &Project, owner: Id, endpoint: &OccurrenceEndpoint) -> String { + std::iter::once(&owner) + .chain(endpoint.references.iter()) + .chain(std::iter::once(&endpoint.occurrence)) + .map(|id| { + p.elements + .get(id) + .map(|e| format!("{} (@{})", e.name, e.alias)) + .unwrap_or_else(|| format!("Unavailable {id}")) + }) + .collect::>() + .join(" → ") +} +struct AddressRow { + endpoint: OccurrenceEndpoint, + reference: bool, +} +fn rows(p: &Project, owner: Id, scope: &[Id], search: &str) -> Result, String> { + let index = TopologyIndex::new(p); + let sequence = index + .resolve_scope(owner, scope) + .map_err(|e| e.to_string())?; + let query = search.trim().to_lowercase(); + Ok(index + .contributions(sequence) + .into_iter() + .filter_map(|occurrence| { + let endpoint = OccurrenceEndpoint { + references: scope.to_vec(), + occurrence, + }; + if !address_label(p, owner, &endpoint) + .to_lowercase() + .contains(&query) + { + return None; + } + let reference = matches!( + p.elements.get(&occurrence).and_then(|e| e.shared()), + Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { sequence: Some(_) }, + .. + }) + ); + Some(AddressRow { + endpoint, + reference, + }) + }) + .collect()) +} +fn event(value: Event) -> Message { + super::event(super::Event::Contextual(value)) +} + +pub(super) fn preview( + index: &TopologyIndex<'_>, + p: &Project, + owner: Id, + reference: Id, + expanded: &BTreeSet, + limit: usize, +) -> Vec { + let children = |endpoint: &OccurrenceEndpoint| { + if index.resolve(owner, endpoint).is_err() { + return vec![]; + } + let sequence = p + .elements + .get(&endpoint.occurrence) + .and_then(|e| e.shared()) + .and_then(|body| match body { + SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { sequence }, + .. + } => *sequence, + _ => None, + }); + let Some(sequence) = sequence else { + return vec![]; + }; + let available: BTreeSet<_> = index.contributions(sequence).into_iter().collect(); + let mut references = endpoint.references.clone(); + references.push(endpoint.occurrence); + p.elements + .get(&sequence) + .and_then(|e| e.steps()) + .into_iter() + .flatten() + .filter(|id| available.contains(id)) + .map(|occurrence| OccurrenceEndpoint { + references: references.clone(), + occurrence: *occurrence, + }) + .collect::>() + }; + let root = OccurrenceEndpoint { + references: vec![], + occurrence: reference, + }; + let mut pending = children(&root); + pending.reverse(); + let mut visible = vec![]; + while let Some(endpoint) = pending.pop() { + if visible.len() == limit { + break; + } + if index.resolve(owner, &endpoint).is_err() { + continue; + } + if expanded.contains(&endpoint) { + pending.extend(children(&endpoint).into_iter().rev()); + } + visible.push(endpoint); + } + visible +} +pub(super) fn view<'a>(p: &'a Project, owner: Id, d: &'a Draft) -> Element<'a, Message> { + let mut out = column![ + text("Choose each endpoint in its caller context. Enter a reference to browse all its contributions; repeated uses stay distinct. Browsing preserves both selected endpoints.").size(12), + row![button("Choose source").style(if !d.target_side { button::primary } else { style::nav }).on_press(event(Event::Side(false))), button("Choose target").style(if d.target_side { button::primary } else { style::nav }).on_press(event(Event::Side(true)))].spacing(6), + text(format!("Source: {}", d.source.as_ref().map(|a| address_label(p,owner,a)).unwrap_or("Choose a contribution".into()))).size(12), + text(format!("Target: {}", d.target.as_ref().map(|a| address_label(p,owner,a)).unwrap_or("Choose a contribution".into()))).size(12), + pick_list(vec![Purpose::Precedence,Purpose::Exchange,Purpose::CommonWork],Some(d.purpose),|p| event(Event::Purpose(p))).width(Fill).text_size(12).padding(8) + ].spacing(7); + match d.purpose { + Purpose::Precedence => { + out = out.push( + text_input("Precedence guard (optional)", &d.guard) + .on_input(|v| event(Event::Guard(v))) + .padding(8) + .size(12), + ) + } + purpose => { + let choices = if purpose == Purpose::Exchange { + targets(p, owner, Tool::Exchange) + } else { + targets(p, owner, Tool::Work) + }; + let selected = d.evidence.map(|id| Choice { + id, + label: p.named(id).to_owned(), + }); + out = out + .push( + pick_list(choices, selected, |c| event(Event::Evidence(c))) + .placeholder("Choose exact engineering identity…") + .width(Fill) + .text_size(12) + .padding(8), + ) + .push( + text("Assembly incidence does not imply execution order or fuse executions.") + .size(11), + ); + } + } + out = out.push( + text_input("Search all contributions in this scope…", &d.search) + .on_input(|v| event(Event::Search(v))) + .padding(7) + .size(12), + ); + let scope_label = std::iter::once(&owner) + .chain(d.scope.iter()) + .map(|id| { + p.elements + .get(id) + .map(|e| format!("{} (@{})", e.name, e.alias)) + .unwrap_or_else(|| format!("Unavailable {id}")) + }) + .collect::>() + .join(" → "); + out = out + .push(text(format!("Browsing: {scope_label}")).size(12)) + .push( + row![ + button("Root").on_press_maybe((!d.scope.is_empty()).then(|| event(Event::Root))), + button("Up one reference") + .on_press_maybe((!d.scope.is_empty()).then(|| event(Event::Up))) + ] + .spacing(6), + ); + let rows = match rows(p, owner, &d.scope, &d.search) { + Ok(rows) => rows, + Err(error) => return out.push(text(error).size(12)).into(), + }; + let range = super::view::choice_page(rows.len(), d.page); + let page = range.start / 24; + out = out.push( + row![ + button("Previous").on_press_maybe((page > 0).then(|| event(Event::Page(page - 1)))), + text(format!( + "{} matching contributions · page {}", + rows.len(), + page + 1 + )) + .size(11), + button("Next") + .on_press_maybe((range.end < rows.len()).then(|| event(Event::Page(page + 1)))) + ] + .spacing(6), + ); + for r in &rows[range] { + let mut line = row![ + button(text(address_label(p, owner, &r.endpoint)).size(12)) + .width(Fill) + .on_press(event(Event::Pick(r.endpoint.clone()))) + ] + .spacing(5); + if r.reference { + line = line + .push(button("Enter reference").on_press(event(Event::Expand(r.endpoint.clone())))); + } + out = out.push(line); + } + out.into() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::document::Document; + use oa_core::source_project::ProjectLimits; + + fn fixture(layer: Layer) -> (Project, Id, Id, Id, Id, Id) { + let f = super::super::direct_tests::fixture(layer); + let mut p = f.p; + let sequence = p + .make_element( + if layer == Layer::Oa { + Kind::Process + } else { + Kind::FunctionalChain + }, + layer, + "Reusable check", + None, + ) + .unwrap(); + let sequence_id = sequence.id; + p = execute_batch(&p, &[Command::CreateElement(sequence)]).unwrap(); + let leaf = occurrence( + &p, + sequence_id, + "Verify pressure", + OccurrenceBody::Action { + action: Some(f.work[0]), + lifeline: None, + }, + ); + let leaf_id = leaf.id; + p = execute_batch(&p, &[Command::CreateElement(leaf)]).unwrap(); + let a = occurrence( + &p, + f.owner, + "First check", + OccurrenceBody::SequenceReference { + sequence: Some(sequence_id), + }, + ); + let b = occurrence( + &p, + f.owner, + "Second check", + OccurrenceBody::SequenceReference { + sequence: Some(sequence_id), + }, + ); + let (a_id, b_id) = (a.id, b.id); + p = execute_batch(&p, &[Command::CreateElement(a), Command::CreateElement(b)]).unwrap(); + (p, f.owner, a_id, b_id, leaf_id, f.work[0]) + } + #[test] + fn qualified_link_gui_repeated_sources_remain_distinct_and_apply_has_exact_source_history() { + for layer in [Layer::Oa, Layer::Sa] { + let (p, owner, a, b, leaf, work) = fixture(layer); + let mut state = State::default(); + state + .update( + &p, + super::super::Event::Start(owner, Tool::Contextual, None), + ) + .unwrap(); + let source = OccurrenceEndpoint { + references: vec![a], + occurrence: leaf, + }; + let target = OccurrenceEndpoint { + references: vec![b], + occurrence: leaf, + }; + for event in [ + Event::Pick(source.clone()), + Event::Side(true), + Event::Pick(target.clone()), + Event::Purpose(Purpose::CommonWork), + Event::Evidence(Choice { + id: work, + label: String::new(), + }), + ] { + state + .update(&p, super::super::Event::Contextual(event)) + .unwrap(); + } + let raw = state.recovery_snapshot().unwrap().unwrap(); + let restored = State::from_recovery(raw.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw)); + let command = restored.prepare_pending(&p).unwrap(); + let Command::CreateRelation(record) = &command else { + panic!("one caller-owned record") + }; + assert_eq!( + record.body, + RelationBody::ContextualLink { + owner, + source: source.clone(), + target: target.clone(), + purpose: ContextualLinkPurpose::CommonWork { work } + } + ); + let expected = execute_batch(&p, std::slice::from_ref(&command)).unwrap(); + let mut doc = Document::from_project(p.clone(), ProjectLimits::default()).unwrap(); + let before = doc.bundle().clone(); + doc.execute(command.clone()).unwrap(); + assert_eq!(doc.project(), &expected); + assert_eq!( + doc.project().elements, + p.elements, + "no copied work or source occurrences" + ); + let after = doc.bundle().clone(); + let reopened = Document::from_bundle(after.clone(), ProjectLimits::default()).unwrap(); + assert_eq!(reopened.project(), &expected); + assert!(doc.undo()); + assert_eq!(doc.bundle(), &before); + assert!(doc.redo()); + assert_eq!(doc.bundle(), &after); + assert_ne!( + address_label(&p, owner, &source), + address_label(&p, owner, &target) + ); + assert!( + Graph::new(&expected, owner, record.id) + .edges + .iter() + .any(|edge| edge.id == record.id && edge.incidence) + ); + } + } + #[test] + fn qualified_link_gui_rejected_endpoint_keeps_exact_raw_intent_after_recovery() { + let (p, owner, a, _, leaf, _) = fixture(Layer::Oa); + let mut state = State::default(); + state + .update( + &p, + super::super::Event::Start(owner, Tool::Contextual, None), + ) + .unwrap(); + for event in [ + Event::Pick(OccurrenceEndpoint { + references: vec![a], + occurrence: leaf, + }), + Event::Side(true), + Event::Pick(OccurrenceEndpoint { + references: vec![Id::new_v4()], + occurrence: leaf, + }), + Event::Guard(" pressure > && ".into()), + ] { + state + .update(&p, super::super::Event::Contextual(event)) + .unwrap(); + } + let before = state.recovery_snapshot().unwrap(); + assert!(state.prepare_pending(&p).is_err()); + assert_eq!(state.recovery_snapshot().unwrap(), before); + let restored = State::from_recovery(before.clone().unwrap()).unwrap(); + assert!(restored.prepare_pending(&p).is_err()); + assert_eq!(restored.recovery_snapshot().unwrap(), before); + } + #[test] + fn qualified_link_gui_deleted_transient_selection_does_not_seed_or_hide_retained_authored_endpoint() + { + let (p, owner, reference, _, leaf, _) = fixture(Layer::Oa); + let endpoint = OccurrenceEndpoint { + references: vec![reference], + occurrence: leaf, + }; + let mut state = State::default(); + state.select_context(oa_core::topology_context::ContextAddress { + owner, + endpoint: endpoint.clone(), + }); + state + .update( + &p, + super::super::Event::Start(owner, Tool::Contextual, None), + ) + .unwrap(); + let before = state.recovery_snapshot().unwrap(); + let deleted = execute_batch(&p, &[Command::Delete(leaf)]).unwrap(); + assert!(state.qualified_selection(&deleted, owner).is_none()); + assert_eq!( + state.recovery_snapshot().unwrap(), + before, + "Authored endpoint is retained for explicit repair" + ); + assert!(state.prepare_pending(&deleted).is_err()); + state.remember( + owner, + reference, + super::super::direct::referenced(&p, owner, reference).unwrap(), + ); + assert_eq!(state.back(&deleted), Some((owner, reference))); + assert!(state.selected_context.is_none()); + state.draft = None; + state + .update( + &deleted, + super::super::Event::Start(owner, Tool::Contextual, None), + ) + .unwrap(); + assert!( + state + .draft + .as_ref() + .unwrap() + .contextual + .as_ref() + .unwrap() + .source + .is_none() + ); + } + #[test] + fn qualified_link_gui_scope_paging_and_search_reach_tail_beyond_512_without_losing_endpoints() { + let (p, owner, reference, _, leaf, _) = fixture(Layer::Oa); + let sequence = p.elements[&leaf].parent.unwrap(); + let mut commands = Vec::new(); + let mut tails = Vec::new(); + for (scope_index, scope) in [owner, sequence].into_iter().enumerate() { + for n in 0..520 { + let mut record = occurrence( + &p, + scope, + &format!("Contribution {n:04}"), + OccurrenceBody::Control { + kind: ControlKind::Choice, + }, + ); + record.alias = format!("scope_{scope_index}_contribution_{n:04}"); + if n == 519 { + tails.push(record.id); + } + commands.push(Command::CreateElement(record)); + } + } + let p = execute_batch(&p, &commands).unwrap(); + let all = rows(&p, owner, &[], "").unwrap(); + assert!(all.len() > 512); + let last_page = super::super::view::choice_page(all.len(), usize::MAX); + assert!( + all[last_page] + .iter() + .any(|row| row.endpoint.occurrence == tails[0]) + ); + let found = rows(&p, owner, &[], "Contribution 0519").unwrap(); + assert_eq!(found.len(), 1); + assert_eq!(found[0].endpoint.occurrence, tails[0]); + let mut draft = Draft::default(); + draft.update(Event::Pick(OccurrenceEndpoint { + references: vec![reference], + occurrence: leaf, + })); + draft.update(Event::Side(true)); + draft.update(Event::Pick(found[0].endpoint.clone())); + let chosen = (draft.source.clone(), draft.target.clone()); + draft.update(Event::Expand(OccurrenceEndpoint { + references: vec![], + occurrence: reference, + })); + draft.update(Event::Search("Contribution 0519".into())); + let nested = rows(&p, owner, &draft.scope, &draft.search).unwrap(); + assert_eq!(nested.len(), 1); + assert_eq!( + nested[0].endpoint, + OccurrenceEndpoint { + references: vec![reference], + occurrence: tails[1] + } + ); + let saved = serde_json::to_value(&draft).unwrap(); + let mut restored: Draft = serde_json::from_value(saved.clone()).unwrap(); + assert_eq!(serde_json::to_value(&restored).unwrap(), saved); + restored.update(Event::Up); + assert!(restored.scope.is_empty()); + assert_eq!((restored.source.clone(), restored.target.clone()), chosen); + assert!(restored.command(&p, owner, "Tail constraint").is_ok()); + let mut historical = saved; + historical.as_object_mut().unwrap().remove("scope"); + let restored: Draft = serde_json::from_value(historical).unwrap(); + assert!(restored.scope.is_empty()); + assert_eq!((restored.source, restored.target), chosen); + } + #[test] + fn qualified_link_gui_nested_preview_folds_only_the_exact_reference_instance() { + let (mut p, owner, a, b, leaf, _) = fixture(Layer::Oa); + let source = p.elements[&leaf].parent.unwrap(); + let deep = p + .make_element(Kind::Process, Layer::Oa, "Nested definition", None) + .unwrap(); + let deep_id = deep.id; + p = execute_batch(&p, &[Command::CreateElement(deep)]).unwrap(); + let deep_leaf = occurrence( + &p, + deep_id, + "Nested control", + OccurrenceBody::Control { + kind: ControlKind::Choice, + }, + ); + let deep_leaf_id = deep_leaf.id; + let nested = occurrence( + &p, + source, + "Nested use", + OccurrenceBody::SequenceReference { + sequence: Some(deep_id), + }, + ); + let nested_id = nested.id; + p = execute_batch( + &p, + &[ + Command::CreateElement(deep_leaf), + Command::CreateElement(nested), + ], + ) + .unwrap(); + let path = OccurrenceEndpoint { + references: vec![a], + occurrence: nested_id, + }; + let mut state = State::default(); + assert!(state.expand_context(&p, owner, path.clone())); + state.expanded.insert(b); + let graph = Graph::expanded_qualified( + &p, + owner, + owner, + &state.expanded, + &state.contextual_expanded, + ); + assert!( + graph + .nodes + .iter() + .find(|n| n.id == a) + .unwrap() + .preview + .iter() + .any(|(v, _)| v.references == vec![a, nested_id] && v.occurrence == deep_leaf_id) + ); + assert!( + !graph + .nodes + .iter() + .find(|n| n.id == b) + .unwrap() + .preview + .iter() + .any(|(v, _)| v.occurrence == deep_leaf_id) + ); + assert!(state.expand_context(&p, owner, path)); + let graph = Graph::expanded_qualified( + &p, + owner, + owner, + &state.expanded, + &state.contextual_expanded, + ); + assert!( + !graph + .nodes + .iter() + .any(|n| n.preview.iter().any(|(v, _)| v.occurrence == deep_leaf_id)) + ); + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct.rs new file mode 100644 index 00000000..f2cbf4f7 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct.rs @@ -0,0 +1,988 @@ +//! Local pointer state and ordinary command proposals for the derived process graph. +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Context { + pub document: Id, + pub owner: Id, + pub revision: u64, + pub epoch: u64, +} +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Insertion { + pub link: Option, + pub before: Option, +} +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum CanvasTool { + #[default] + Select, + Add(Tool, ControlKind), + Connect, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum End { + Source, + Target, +} +#[derive(Debug, Clone)] +pub enum CanvasEvent { + Scrolled(Context, scenario::ScrollArea, iced::Vector), + Choose(Context, CanvasTool), + Select(Context, Id), + SelectQualified(Context, OccurrenceEndpoint), + ExpandQualified(Context, OccurrenceEndpoint), + OpenQualified(Context, OccurrenceEndpoint), + AppendExchange(Context), + Begin { + context: Context, + tool: Tool, + control: ControlKind, + insertion: Insertion, + }, + Connect { + context: Context, + source: Id, + target: Id, + }, + Reconnect { + context: Context, + link: Id, + end: End, + target: Id, + }, + Reorder { + context: Context, + occurrence: Id, + before: Id, + }, + Expand(Context, Id), + Open(Context, Id), + Back(Context), + Zoom(Context, Option), + Camera(Context, u64, super::super::canvas_workspace::Event), + Focus(Context), + Viewport(Context, Size), +} +impl CanvasEvent { + pub fn context(&self) -> Context { + match self { + Self::Scrolled(c, ..) + | Self::Choose(c, ..) + | Self::Select(c, ..) + | Self::SelectQualified(c, ..) + | Self::ExpandQualified(c, ..) + | Self::OpenQualified(c, ..) + | Self::Expand(c, ..) + | Self::Open(c, ..) + | Self::Back(c) + | Self::AppendExchange(c) + | Self::Zoom(c, ..) + | Self::Camera(c, ..) + | Self::Focus(c) + | Self::Viewport(c, ..) => *c, + Self::Begin { context, .. } + | Self::Connect { context, .. } + | Self::Reconnect { context, .. } + | Self::Reorder { context, .. } => *context, + } + } +} +pub(super) struct ReturnFrame { + identity: Option<(Id, Id)>, + owner: Id, + occurrence: Id, + target: Id, + address: Option, + pan: iced::Vector, + zoom: Option, + fit: bool, + viewport: Option, + expanded: BTreeSet, + contextual_expanded: BTreeSet, +} +impl State { + pub(crate) fn select_context(&mut self, address: oa_core::topology_context::ContextAddress) { + self.selected_context = Some(address); + } + pub(crate) fn clear_context(&mut self) { + self.selected_context = None; + } + pub(super) fn qualified_selection( + &self, + p: &Project, + owner: Id, + ) -> Option<&oa_core::topology_context::ContextAddress> { + self.selected_context.as_ref().filter(|address| { + address.owner == owner + && oa_core::topology_context::TopologyIndex::new(p) + .resolve(owner, &address.endpoint) + .is_ok() + }) + } + pub(crate) fn expand_context( + &mut self, + p: &Project, + owner: Id, + endpoint: OccurrenceEndpoint, + ) -> bool { + if oa_core::topology_context::TopologyIndex::new(p) + .resolve(owner, &endpoint) + .is_err() + || !matches!( + p.elements + .get(&endpoint.occurrence) + .and_then(|e| e.shared()), + Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { sequence: Some(_) }, + .. + }) + ) + { + return false; + } + if let Some(root) = endpoint.references.first() { + self.expanded.insert(*root); + } + if !self.contextual_expanded.remove(&endpoint) { + self.contextual_expanded.insert(endpoint); + } + true + } + pub(crate) fn enter(&mut self, document: Id, owner: Id) { + if self.identity != Some((document, owner)) { + self.identity = Some((document, owner)); + self.tool = CanvasTool::Select; + self.pan = iced::Vector::ZERO; + self.zoom = Some(1.); + self.fit = false; + self.viewport = None; + self.expanded.clear(); + self.scroll_offsets = [iced::Vector::ZERO; 2]; + } + } + #[cfg(test)] + pub(crate) fn scroll_offset(&self) -> iced::Vector { + self.scroll_offsets[0] + self.scroll_offsets[1] + } + pub(crate) fn scrolled(&mut self, area: scenario::ScrollArea, offset: iced::Vector) -> bool { + let index = match area { + scenario::ScrollArea::Canvas => 0, + scenario::ScrollArea::Page => 1, + }; + let changed = self.scroll_offsets[index] != offset; + self.scroll_offsets[index] = offset; + changed + } + pub(crate) fn choose(&mut self, tool: CanvasTool) { + // Choosing how to edit must retain the user's current view. + self.tool = tool; + } + pub(crate) fn camera(&mut self, event: super::super::canvas_workspace::Event) -> bool { + super::super::canvas_workspace::change(&mut self.pan, &mut self.zoom, &mut self.fit, event) + } + pub(crate) fn zoom(&mut self, value: Option) { + self.fit = value.is_none(); + self.zoom = value.map(|v| v.clamp(0.05, 3.)); + } + pub(crate) fn viewport(&mut self, size: Size) -> bool { + if !size.width.is_finite() + || !size.height.is_finite() + || size.width <= 0. + || size.height <= 0. + { + return false; + } + let changed = self.viewport != Some(size); + self.viewport = Some(size); + changed + } + pub(crate) fn expand(&mut self, p: &Project, owner: Id, id: Id) -> bool { + if !is_contribution(p, owner, id) + || !matches!( + p.elements[&id].shared(), + Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { .. }, + .. + }) + ) + { + return false; + } + if !self.expanded.remove(&id) { + self.expanded.insert(id); + } + true + } + pub(crate) fn remember(&mut self, owner: Id, occurrence: Id, target: Id) { + if self.return_to.len() == 32 { + self.return_to.remove(0); + } + self.return_to.push(ReturnFrame { + identity: self.identity, + owner, + occurrence, + target, + address: self.selected_context.clone(), + pan: self.pan, + zoom: self.zoom, + fit: self.fit, + viewport: self.viewport, + expanded: self.expanded.clone(), + contextual_expanded: self.contextual_expanded.clone(), + }); + } + pub(crate) fn back(&mut self, p: &Project) -> Option<(Id, Id)> { + while let Some(frame) = self.return_to.pop() { + if frame.identity.map(|i| i.0) != self.identity.map(|i| i.0) { + continue; + } + let valid = referenced(p, frame.owner, frame.occurrence) == Some(frame.target) + || if let Some(address) = &frame.address { + oa_core::topology_context::TopologyIndex::new(p) + .resolve(frame.owner, &address.endpoint) + .is_ok_and(|r| r.defining_sequence == frame.target) + } else { + false + }; + if valid && is_contribution(p, frame.owner, frame.occurrence) { + self.identity = frame.identity; + self.selected_context = frame.address.filter(|a| { + oa_core::topology_context::TopologyIndex::new(p) + .resolve(frame.owner, &a.endpoint) + .is_ok() + }); + self.pan = frame.pan; + self.zoom = frame.zoom; + self.fit = frame.fit; + self.viewport = frame.viewport; + self.expanded = frame.expanded; + self.contextual_expanded = frame.contextual_expanded; + return Some((frame.owner, frame.occurrence)); + } + } + None + } + pub(crate) fn camera_zoom(&mut self, p: &Project, owner: Id, zoom: Option) { + let graph = + Graph::expanded_qualified(p, owner, owner, &self.expanded, &self.contextual_expanded); + let old = self.scale(graph.size, 500.); + if let Some(next) = zoom { + let size = self.viewport.unwrap_or(Size::new(500., 300.)); + self.camera(super::super::canvas_workspace::Event::Zoom { + factor: next / old, + at: Point::new(size.width / 2., size.height / 2.), + base: old, + }); + } else { + self.zoom(None); + let fitted = self.scale(graph.size, 500.); + self.zoom(Some(fitted)); + self.pan = iced::Vector::ZERO; + } + } + pub(crate) fn created_selection(&self, p: &Project, command: &Command) -> Option { + let owner = self.pending_owner()?; + if self + .draft + .as_ref() + .is_some_and(|d| d.source.label(d.target).is_some()) + && self.prepare_pending(p).ok().as_ref() != Some(command) + { + return None; + } + if let Command::CreateRelation(r) | Command::EditRelation(_, r) = command + && matches!(&r.body, RelationBody::ContextualLink { owner: caller, .. } if *caller == owner) + { + return Some(r.id); + } + let Command::Batch(commands) = command else { + return None; + }; + commands.iter().find_map(|c| match c { + Command::CreateElement(e) if e.parent == Some(owner) => Some(e.id), + _ => None, + }) + } + pub(crate) fn begin( + &mut self, + p: &Project, + context: Context, + tool: Tool, + control: ControlKind, + insertion: Insertion, + ) -> Result<(), String> { + if self.draft.is_some() { + return Err(PENDING_DRAFT.into()); + } + if !matches!( + tool, + Tool::Work | Tool::Exchange | Tool::Reuse | Tool::Control + ) { + return Err("Choose a contribution tool.".into()); + } + let order = legacy_order(p, context.owner); + let link = insertion + .link + .map(|id| { + p.relations + .get(&id) + .filter(|r| { + r.kind() == RelationKind::SequenceLink + && is_contribution(p, context.owner, r.source()) + && is_contribution(p, context.owner, r.target()) + }) + .ok_or("Choose a sequence link in this process.") + }) + .transpose()?; + if insertion + .before + .is_some_and(|id| !is_contribution(p, context.owner, id)) + { + return Err("Choose a presentation slot in this process.".into()); + } + self.update(p, Event::Start(context.owner, tool, None))?; + let d = self.draft.as_mut().expect("started form"); + d.fixed = true; + d.insertion = insertion; + d.control = control; + if let Some(r) = link { + d.before = Some(r.source()); + d.after = Some(r.target()); + } else if !order.is_empty() { + let index = insertion + .before + .and_then(|id| order.iter().position(|v| *v == id)) + .unwrap_or(order.len()); + d.before = index.checked_sub(1).map(|i| order[i]); + d.after = order.get(index).copied(); + } + Ok(()) + } + pub(crate) fn append_exchange(&mut self, p: &Project, owner: Id) -> Result<(), String> { + let last = legacy_order(p, owner) + .last() + .copied() + .ok_or("Choose an ordered exchange path to append to.")?; + self.update(p, Event::Start(owner, Tool::Exchange, Some(last)))?; + self.draft.as_mut().expect("started form").fixed = true; + Ok(()) + } + pub(crate) fn connect( + &mut self, + p: &Project, + owner: Id, + source: Id, + target: Id, + ) -> Result<(), String> { + if self.draft.is_some() { + return Err(PENDING_DRAFT.into()); + } + if !is_contribution(p, owner, source) + || !is_contribution(p, owner, target) + || !validation::can_connect(p, RelationKind::SequenceLink, source, target) + { + return Err("Choose compatible contributions directly owned by this process.".into()); + } + self.update(p, Event::Start(owner, Tool::Connect, None))?; + let d = self.draft.as_mut().expect("started form"); + d.fixed = true; + d.before = Some(source); + d.after = Some(target); + Ok(()) + } +} +pub(crate) fn referenced(p: &Project, owner: Id, occurrence: Id) -> Option { + if !is_contribution(p, owner, occurrence) { + return None; + } + match p.elements[&occurrence].shared()? { + SharedBody::Occurrence { + body: OccurrenceBody::Action { action, .. }, + .. + } => *action, + SharedBody::Occurrence { + body: OccurrenceBody::Exchange { exchange, .. }, + .. + } => *exchange, + SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { sequence }, + .. + } => *sequence, + _ => None, + } +} +pub(crate) fn reconnect( + p: &Project, + owner: Id, + id: Id, + end: End, + endpoint: Id, +) -> Result { + let mut r = p + .relations + .get(&id) + .filter(|r| { + r.kind() == RelationKind::SequenceLink + && is_contribution(p, owner, r.source()) + && is_contribution(p, owner, r.target()) + }) + .ok_or("Choose a sequence link in this process.")? + .clone(); + if !is_contribution(p, owner, endpoint) { + return Err("Choose a local contribution endpoint.".into()); + } + if let RelationBody::SequenceLink { source, target, .. } = &mut r.body { + match end { + End::Source => *source = endpoint, + End::Target => *target = endpoint, + } + } + let command = Command::EditRelation(id, r); + execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?; + Ok(command) +} +pub(crate) fn reorder(p: &Project, owner: Id, id: Id, before: Id) -> Result { + if !is_contribution(p, owner, id) || !is_contribution(p, owner, before) { + return Err("Reorder contributions in this process only.".into()); + } + let graph = Graph::new(p, owner, id); + let position = |id| graph.nodes.iter().find(|n| n.id == id).map(|n| n.rect.x); + if position(id) != position(before) { + return Err("Reorder within this column; links determine the path columns.".into()); + } + let mut steps = p.elements[&owner].steps().unwrap().clone(); + if id != before { + steps.retain(|v| *v != id); + let index = steps + .iter() + .position(|v| *v == before) + .ok_or("This slot no longer exists.")?; + steps.insert(index, id); + } + let command = Command::SetSteps(owner, steps); + execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?; + Ok(command) +} + +#[derive(Default)] +pub(super) struct Input { + context: Option, + tool: CanvasTool, + pointer: Option, + bounds: Option, + source: Option, + press: Option, +} +#[derive(Clone, Copy)] +enum Press { + Connect { + source: Id, + first: bool, + start: Point, + moved: bool, + }, + Reconnect { + link: Id, + end: End, + start: Point, + moved: bool, + }, + Reorder { + occurrence: Id, + start: Point, + moved: bool, + }, + Add { + insertion: Insertion, + }, +} +impl Input { + fn cancel(&mut self) { + self.source = None; + self.press = None; + } +} +impl Graph<'_> { + fn ordered_path_at(&self, point: Point) -> Option<&OrderedPath> { + self.ordered_paths.iter().find(|path| { + path.points.windows(2).any(|pair| { + crate::diagram::distance_to_segment(point, pair[0], pair[1]) < 8. / self.scale + }) + }) + } + fn node_at(&self, point: Point) -> Option { + self.nodes + .iter() + .rev() + .find(|n| n.rect.contains(point)) + .map(|n| n.id) + } + fn qualified_at(&self, point: Point) -> Option { + self.nodes.iter().find_map(|node| { + node.preview.iter().enumerate().find_map(|(i, (id, _))| { + let rect = Rectangle::new( + Point::new( + node.rect.x + 12. + (i % 2) as f32 * 125., + node.rect.y + 118. + (i / 2) as f32 * 44., + ), + Size::new(113., 32.), + ); + rect.contains(point).then(|| id.clone()) + }) + }) + } + fn edge_at(&self, point: Point) -> Option { + self.edges + .iter() + .find(|e| { + (!e.guard.is_empty() && e.label.contains(point)) + || e.points.windows(2).any(|v| { + crate::diagram::distance_to_segment(point, v[0], v[1]) < 8. / self.scale + }) + }) + .map(|e| e.id) + } + fn contextual_edge_at(&self, point: Point) -> Option { + self.edges + .iter() + .filter(|edge| edge.layer == EdgeLayer::Contextual) + .find(|edge| { + edge.label.contains(point) + || edge.points.windows(2).any(|segment| { + crate::diagram::distance_to_segment(point, segment[0], segment[1]) + < 8. / self.scale + }) + }) + .map(|edge| edge.id) + } + fn handle(&self, id: Id) -> Option { + let n = self.nodes.iter().find(|n| n.id == id)?; + Some(Rectangle::new( + Point::new(n.rect.x + n.rect.width - 30., n.rect.y + 7.), + Size::new(23., 24.), + )) + } + fn endpoint_at(&self, point: Point) -> Option<(Id, End)> { + if self.project.relations.get(&self.selected)?.kind() != RelationKind::SequenceLink { + return None; + } + let e = self.edges.iter().find(|e| e.id == self.selected)?; + [(e.points[0], End::Source), (*e.points.last()?, End::Target)] + .into_iter() + .find(|(p, _)| p.distance(point) < 10. / self.scale) + .map(|(_, end)| (e.id, end)) + } + fn connectable(&self, source: Id, target: Id) -> bool { + validation::can_connect(self.project, RelationKind::SequenceLink, source, target) + } + pub(super) fn input( + &self, + state: &mut Input, + event: &canvas::Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Option> { + let (context, tool, editable) = self + .interaction + .map_or((None, CanvasTool::Select, false), |(c, t, e)| { + (Some(c), t, e) + }); + if state.context != context + || state.tool != tool + || state.bounds.is_some_and(|b| b != bounds) + { + state.cancel(); + state.pointer = None; + state.context = context; + state.tool = tool; + } + state.bounds = Some(bounds); + if !editable { + state.cancel(); + } + let visible = cursor.position().is_some(); + if !visible + && matches!( + event, + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + ) + { + state.cancel(); + return Some(Action::request_redraw()); + } + let cursor = match event { + canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => { + state.pointer = Some(*position); + if let Some( + Press::Connect { start, moved, .. } + | Press::Reconnect { start, moved, .. } + | Press::Reorder { start, moved, .. }, + ) = &mut state.press + { + *moved |= position.distance(*start) >= 3.; + } + mouse::Cursor::Available(*position + self.scroll_offset) + } + canvas::Event::Mouse(mouse::Event::WheelScrolled { .. } | mouse::Event::CursorLeft) + | canvas::Event::Window( + iced::window::Event::Resized(_) | iced::window::Event::Unfocused, + ) => { + state.cancel(); + state.pointer = None; + return Some(Action::request_redraw()); + } + canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + .. + }) => { + state.cancel(); + return Some(Action::request_redraw().and_capture()); + } + canvas::Event::Mouse( + mouse::Event::ButtonPressed(_) | mouse::Event::ButtonReleased(_), + ) => state + .pointer + .map_or(cursor, |p| mouse::Cursor::Available(p + self.scroll_offset)), + canvas::Event::Window(iced::window::Event::RedrawRequested(_)) => { + state.pointer = cursor.position().map(|p| p - self.scroll_offset); + cursor + } + _ => cursor, + }; + let world = cursor.position_in(bounds).map(|p| { + Point::new( + (p.x - self.camera_pan.x) / self.scale, + (p.y - self.camera_pan.y) / self.scale, + ) + }); + let publish = |event| Some(Action::publish(Message::TopologyCanvas(event)).and_capture()); + match event { + canvas::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if visible => { + let point = world?; + let Some(context) = context else { + return self + .node_at(point) + .or_else(|| self.edge_at(point)) + .map(|id| Action::publish(Message::Select(id)).and_capture()); + }; + if !editable { + return None; + } + let start = cursor.position()? - self.scroll_offset; + match tool { + CanvasTool::Add(..) => { + state.press = Some(Press::Add { + insertion: Insertion { + link: self + .edge_at(point) + .filter(|_| self.node_at(point).is_none()), + before: self.node_at(point).or_else(|| { + self.ordered_path_at(point).map(|path| path.target) + }), + }, + }); + } + CanvasTool::Connect => { + let first = state.source.is_none(); + let source = state.source.or_else(|| self.node_at(point))?; + state.source = Some(source); + state.press = Some(Press::Connect { + source, + first, + start, + moved: false, + }); + } + CanvasTool::Select => { + if let Some(endpoint) = self.qualified_at(point) { + return publish(CanvasEvent::SelectQualified(context, endpoint)); + } else if let Some((link, end)) = self.endpoint_at(point) { + state.press = Some(Press::Reconnect { + link, + end, + start, + moved: false, + }); + } else if let Some(link) = self.contextual_edge_at(point) { + return publish(CanvasEvent::Select(context, link)); + } else if self + .handle(self.selected) + .is_some_and(|r| r.contains(point)) + { + state.press = Some(Press::Reorder { + occurrence: self.selected, + start, + moved: false, + }); + } else { + return self + .node_at(point) + .or_else(|| self.edge_at(point)) + .and_then(|id| publish(CanvasEvent::Select(context, id))); + } + } + } + Some(Action::request_redraw().and_capture()) + } + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let press = state.press.take()?; + let context = context?; + match press { + Press::Add { insertion } if world.is_some() => { + if let CanvasTool::Add(tool, control) = tool { + state.cancel(); + return publish(CanvasEvent::Begin { + context, + tool, + control, + insertion, + }); + } + } + Press::Connect { + source, + first, + moved, + .. + } if moved || !first => { + if let Some(target) = world + .and_then(|p| self.node_at(p)) + .filter(|target| self.connectable(source, *target)) + { + state.cancel(); + return publish(CanvasEvent::Connect { + context, + source, + target, + }); + } + } + Press::Reconnect { + link, + end, + moved: true, + .. + } => { + if let Some(target) = world.and_then(|p| self.node_at(p)) { + state.cancel(); + return publish(CanvasEvent::Reconnect { + context, + link, + end, + target, + }); + } + } + Press::Reorder { + occurrence, + moved: true, + .. + } => { + if let Some(before) = world.and_then(|p| self.node_at(p)) { + state.cancel(); + return publish(CanvasEvent::Reorder { + context, + occurrence, + before, + }); + } + } + _ => {} + } + Some(Action::request_redraw().and_capture()) + } + canvas::Event::Mouse(mouse::Event::CursorMoved { .. }) => { + Some(Action::request_redraw()) + } + _ => None, + } + } + pub(super) fn overlay( + &self, + state: &Input, + f: &mut Frame, + cursor: mouse::Cursor, + bounds: Rectangle, + ) { + let Some((context, tool, editable)) = self.interaction else { + return; + }; + if let Some(insertion) = self.pending_insertion { + if let Some(edge) = insertion + .link + .and_then(|id| self.edges.iter().find(|e| e.id == id)) + { + draw_arrow(f, &edge.points, style::TEAL, 3.); + } else if let Some(node) = insertion + .before + .and_then(|id| self.nodes.iter().find(|n| n.id == id)) + { + f.stroke( + &Path::rounded_rectangle(node.rect.position(), node.rect.size(), 10.into()), + Stroke::default().with_color(style::TEAL).with_width(3.), + ); + } + } + if !editable { + return; + } + let point = state + .pointer + .map(|p| p + self.scroll_offset) + .or_else(|| cursor.position()) + .filter(|p| bounds.contains(*p)) + .map(|p| { + Point::new( + (p.x - bounds.x - self.camera_pan.x) / self.scale, + (p.y - bounds.y - self.camera_pan.y) / self.scale, + ) + }); + let live = + state.context == Some(context) && state.tool == tool && state.bounds == Some(bounds); + if live && let Some(source) = state.source { + for n in &self.nodes { + if n.id == source || self.connectable(source, n.id) { + f.stroke( + &Path::rounded_rectangle(n.rect.position(), n.rect.size(), 10.into()), + Stroke::default().with_color(style::TEAL).with_width(2.5), + ); + } + } + if let (Some(n), Some(to)) = (self.nodes.iter().find(|n| n.id == source), point) { + draw_arrow( + f, + &[Point::new(n.rect.x + n.rect.width, n.rect.center_y()), to], + style::TEAL, + 2., + ); + } + } + if live + && let Some(Press::Reconnect { link, end, .. }) = state.press + && let Some(edge) = self.edges.iter().find(|e| e.id == link) + { + for node in &self.nodes { + let (source, target) = match end { + End::Source => (node.id, edge.target), + End::Target => (edge.source, node.id), + }; + if self.connectable(source, target) { + f.stroke( + &Path::rounded_rectangle(node.rect.position(), node.rect.size(), 10.into()), + Stroke::default().with_color(style::TEAL).with_width(2.5), + ); + } + } + if let Some(point) = point { + let (from, to) = match end { + End::Source => (point, *edge.points.last().unwrap()), + End::Target => (edge.points[0], point), + }; + draw_arrow(f, &[from, to], style::TEAL, 2.5); + } + } + if live + && let Some(Press::Reorder { occurrence, .. }) = state.press + && let Some(point) = point + && let Some(target) = self.node_at(point) + { + let source = self.nodes.iter().find(|n| n.id == occurrence).unwrap(); + let target = self.nodes.iter().find(|n| n.id == target).unwrap(); + if source.rect.x == target.rect.x { + f.stroke( + &Path::line( + Point::new(target.rect.x, target.rect.y - 7.), + Point::new(target.rect.x + target.rect.width, target.rect.y - 7.), + ), + Stroke::default().with_color(style::TEAL).with_width(3.), + ); + draw_text( + f, + "Order before · paths unchanged".into(), + Point::new(target.rect.x, target.rect.y - 24.), + 12., + style::TEAL, + ); + } + } + if tool == CanvasTool::Select { + if let Some(r) = self.handle(self.selected) { + f.fill_rectangle(r.position(), r.size(), Color::WHITE); + draw_text(f, "↕".into(), r.position(), 17., style::TEAL); + } + if let Some(e) = self.edges.iter().find(|e| e.id == self.selected) { + for p in [e.points[0], *e.points.last().unwrap()] { + f.fill(&Path::circle(p, 6. / self.scale), style::TEAL); + } + } + } + if let CanvasTool::Add(..) = tool + && let Some(point) = point + { + let at = self + .edge_at(point) + .filter(|_| self.node_at(point).is_none()); + let caption = if let Some(id) = at { + if let Some(e) = self.edges.iter().find(|e| e.id == id) { + draw_arrow(f, &e.points, style::TEAL, 3.); + } + "Insert on this path · preserve its guard" + } else if let Some(path) = self.ordered_path_at(point) { + draw_arrow(f, &path.points, style::TEAL, 3.); + "Insert on ordered path · preserve its order" + } else if self.ordered && self.node_at(point).is_some() { + "Insert before this contribution · preserve path" + } else if self.ordered { + "Continue the ordered path" + } else if self.node_at(point).is_some() { + "Add before this contribution · unconnected" + } else { + "Append contribution · unconnected" + }; + draw_text( + f, + caption.into(), + Point::new(point.x + 12., point.y + 16.), + 12. / self.scale, + style::TEAL, + ); + } + } +} + +impl State { + pub(crate) fn reveal_selection(&mut self, p: &Project, owner: Id, id: Id) -> bool { + let graph = + Graph::expanded_qualified(p, owner, id, &self.expanded, &self.contextual_expanded); + let scale = self.scale(graph.size, 500.); + let rect = graph + .nodes + .iter() + .find(|n| n.id == id) + .map(|n| { + self.qualified_selection(p, owner) + .map_or(n.rect, |a| n.endpoint_rect(&a.endpoint)) + }) + .or_else(|| graph.edges.iter().find(|e| e.id == id).map(|e| e.label)); + let Some(rect) = rect else { + return false; + }; + let viewport = self.viewport.unwrap_or(Size::new(500., 300.)); + let x = rect.x * scale + self.pan.x; + let y = rect.y * scale + self.pan.y; + let delta = iced::Vector::new( + if x < 12. || x + rect.width * scale > viewport.width - 12. { + 12. - x + } else { + 0. + }, + if y < 12. || y + rect.height * scale > viewport.height - 12. { + 12. - y + } else { + 0. + }, + ); + self.camera(super::super::canvas_workspace::Event::Pan(delta)) + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct_tests.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct_tests.rs new file mode 100644 index 00000000..6217aabc --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/direct_tests.rs @@ -0,0 +1,1470 @@ +use super::*; +use crate::document::Document; +use oa_core::source_project::{ProjectLimits, SourceBundle}; + +pub(crate) struct Fixture { + pub p: Project, + pub owner: Id, + pub work: [Id; 3], + pub nodes: [Id; 3], + pub exchange: Id, + pub link: Id, +} +pub(crate) fn fixture(layer: Layer) -> Fixture { + let mut p = Project::empty(); + let owner = p + .make_element( + if layer == Layer::Oa { + Kind::Process + } else { + Kind::FunctionalChain + }, + layer, + "Respond safely", + None, + ) + .unwrap(); + let owner_id = owner.id; + p = execute_batch(&p, &[Command::CreateElement(owner)]).unwrap(); + let mut work = Vec::new(); + let mut nodes = Vec::new(); + for name in ["Assess", "Respond", "Recover"] { + let e = p + .make_element( + if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }, + layer, + name, + None, + ) + .unwrap(); + let id = e.id; + p = execute_batch(&p, &[Command::CreateElement(e)]).unwrap(); + work.push(id); + let e = occurrence( + &p, + owner_id, + name, + OccurrenceBody::Action { + action: Some(id), + lifeline: None, + }, + ); + let id = e.id; + p = execute_batch(&p, &[Command::CreateElement(e)]).unwrap(); + nodes.push(id); + } + let kind = if layer == Layer::Oa { + RelationKind::Interaction + } else { + RelationKind::FunctionalExchange + }; + p = execute_batch( + &p, + &[Command::Connect( + kind, + "Assessment".into(), + work[0], + work[1], + )], + ) + .unwrap(); + let exchange = p.relations.values().find(|r| r.kind() == kind).unwrap().id; + let Command::CreateRelation(mut r) = link(&p, nodes[0], nodes[1], "approved") else { + unreachable!() + }; + r.alias = "assessed_path".into(); + r.description = "Reviewed safety rationale".into(); + let link_id = r.id; + p = execute_batch(&p, &[Command::CreateRelation(r)]).unwrap(); + Fixture { + p, + owner: owner_id, + work: work.try_into().unwrap(), + nodes: nodes.try_into().unwrap(), + exchange, + link: link_id, + } +} +pub(crate) fn context(owner: Id) -> Context { + Context { + document: Id::new_v4(), + owner, + revision: 0, + epoch: 0, + } +} +fn command(f: &Fixture, tool: Tool, target: Option, insertion: Insertion) -> Command { + let mut state = State::default(); + state + .begin(&f.p, context(f.owner), tool, ControlKind::Choice, insertion) + .unwrap(); + if let Some(id) = target { + state + .update( + &f.p, + Event::Target(Choice { + id, + label: "Source".into(), + }), + ) + .unwrap(); + } + state.prepare_pending(&f.p).unwrap() +} +fn created(p: &Project, after: &Project, owner: Id) -> Id { + after + .children(owner) + .find(|e| !p.contains(e.id)) + .unwrap() + .id +} +#[test] +fn direct_process_oa_sa_repeated_occurrences_share_sources_and_split_one_guarded_link() { + for layer in [Layer::Oa, Layer::Sa] { + let mut f = fixture(layer); + let original = f.p.clone(); + let add = command(&f, Tool::Work, Some(f.work[0]), Insertion::default()); + let result = execute_batch(&f.p, &[add]).unwrap(); + let repeated = created(&f.p, &result, f.owner); + assert_ne!(repeated, f.nodes[0]); + assert_eq!(result.elements[&f.work[0]], f.p.elements[&f.work[0]]); + assert!( + matches!(result.elements[&repeated].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::Action{action:Some(id),..},..})if *id==f.work[0]) + ); + f.p = result; + let plan = command( + &f, + Tool::Exchange, + Some(f.exchange), + Insertion { + link: Some(f.link), + before: None, + }, + ); + let serialized = serde_json::to_string(&plan).unwrap(); + let api: Command = serde_json::from_str(&serialized).unwrap(); + assert_eq!(api, plan); + let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); + let before = doc.bundle().clone(); + doc.execute(plan).unwrap(); + let inserted = created(&f.p, doc.project(), f.owner); + let r = &doc.project().relations[&f.link]; + let expected = &f.p.relations[&f.link]; + assert_eq!(r.id, expected.id); + assert_eq!(r.alias, expected.alias); + assert_eq!(r.description, expected.description); + assert_eq!(r.source(), expected.source()); + assert_eq!(r.target(), inserted); + assert!(matches!(&r.body,RelationBody::SequenceLink{guard,..}if guard=="approved")); + assert!( + sequence_links(doc.project(), f.owner) + .iter() + .any(|r| r.source() == inserted && r.target() == f.nodes[1]) + ); + let steps = doc.project().elements[&f.owner].steps().unwrap(); + assert_eq!(&steps[..3], &[f.nodes[0], inserted, f.nodes[1]]); + assert_eq!( + doc.project().relations[&f.exchange], + original.relations[&f.exchange] + ); + assert_eq!(doc.project(), &execute_batch(&f.p, &[api]).unwrap()); + let after = doc.bundle().clone(); + assert!(doc.undo()); + assert_eq!(doc.bundle(), &before); + assert!(doc.redo()); + assert_eq!(doc.bundle(), &after); + let reopened = Document::from_bundle(after, ProjectLimits::default()).unwrap(); + assert_eq!(reopened.project(), doc.project()); + } +} +#[test] +fn direct_process_specific_parallel_link_and_incompatible_adjacency() { + let mut f = fixture(Layer::Oa); + let Command::CreateRelation(mut second) = link(&f.p, f.nodes[0], f.nodes[1], "urgent") else { + unreachable!() + }; + second.alias = "urgent_path".into(); + let second_id = second.id; + f.p = execute_batch(&f.p, &[Command::CreateRelation(second)]).unwrap(); + let graph = Graph::new(&f.p, f.owner, f.owner); + for edge in &graph.edges { + let clicked = send( + &graph, + &mut Input::default(), + mouse::Event::ButtonPressed(mouse::Button::Left), + edge.label.center(), + Rectangle::new(Point::ORIGIN, graph.size), + ); + assert!( + matches!(clicked,Some(Message::Select(id))if id==edge.id), + "Each guarded parallel path must be individually selectable" + ); + } + + let plan = command( + &f, + Tool::Control, + None, + Insertion { + link: Some(f.link), + before: None, + }, + ); + let after = execute_batch(&f.p, &[plan]).unwrap(); + assert_eq!(after.relations[&second_id], f.p.relations[&second_id]); + let mut state = State::default(); + state + .begin( + &f.p, + context(f.owner), + Tool::Exchange, + ControlKind::Choice, + Insertion { + link: Some(f.link), + before: None, + }, + ) + .unwrap(); + let wrong = execute_batch( + &f.p, + &[Command::Connect( + RelationKind::Interaction, + "Reverse".into(), + f.work[2], + f.work[0], + )], + ) + .unwrap(); + let wrong_id = wrong + .relations + .values() + .find(|r| r.name == "Reverse") + .unwrap() + .id; + state + .update( + &wrong, + Event::Target(Choice { + id: wrong_id, + label: "Wrong".into(), + }), + ) + .unwrap(); + assert!(state.prepare_pending(&wrong).is_err()); + assert!(state.draft.is_some()); + assert!(direct::reconnect(&f.p, f.owner, f.link, direct::End::Target, f.nodes[0]).is_err()); +} +#[test] +fn direct_process_controls_allow_incomplete_work_and_preserve_branch_semantics() { + let f = fixture(Layer::Oa); + for kind in [ + ControlKind::Choice, + ControlKind::Merge, + ControlKind::Fork, + ControlKind::Join, + ControlKind::Loop, + ] { + let mut state = State::default(); + state + .begin( + &f.p, + context(f.owner), + Tool::Control, + kind, + Insertion::default(), + ) + .unwrap(); + let result = execute_batch(&f.p, &[state.prepare_pending(&f.p).unwrap()]).unwrap(); + let id = created(&f.p, &result, f.owner); + assert!( + matches!(result.elements[&id].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::Control{kind:k},..})if *k==kind) + ); + assert!( + validation::validate(&result) + .iter() + .any(|d| d.element == Some(id) + && d.code == "control_cardinality" + && d.severity == validation::Severity::Warning) + ); + } + for kind in [ControlKind::Choice, ControlKind::Fork] { + let mut state = State::default(); + state + .update(&f.p, Event::Start(f.owner, Tool::Branch, Some(f.nodes[0]))) + .unwrap(); + for event in [ + Event::Target(Choice { + id: f.work[0], + label: "A".into(), + }), + Event::Alternate(Choice { + id: f.work[2], + label: "B".into(), + }), + Event::Control(ControlChoice(kind)), + Event::Guard("ready".into()), + ] { + state.update(&f.p, event).unwrap(); + } + let result = execute_batch(&f.p, &[state.prepare_pending(&f.p).unwrap()]).unwrap(); + assert!( + matches!(&result.relations[&f.link].body,RelationBody::SequenceLink{guard,..}if guard=="approved") + ); + let branch_guards: Vec<_> = sequence_links(&result, f.owner) + .into_iter() + .filter(|r| !f.p.relations.contains_key(&r.id)) + .filter_map(|r| { + if let RelationBody::SequenceLink { guard, .. } = &r.body { + Some(guard.as_str()) + } else { + None + } + }) + .collect(); + assert_eq!( + branch_guards.contains(&"ready"), + kind == ControlKind::Choice + ); + assert_eq!(branch_guards.contains(&"else"), kind == ControlKind::Choice); + } +} +#[test] +fn direct_process_reconnect_delete_and_reorder_preserve_other_paths_and_sources() { + let f = fixture(Layer::Oa); + let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); + let original = doc.bundle().clone(); + doc.execute( + direct::reconnect( + doc.project(), + f.owner, + f.link, + direct::End::Target, + f.nodes[2], + ) + .unwrap(), + ) + .unwrap(); + assert_eq!(doc.project().relations[&f.link].target(), f.nodes[2]); + assert_eq!(doc.project().relations[&f.link].alias, "assessed_path"); + let reconnected = doc.bundle().clone(); + doc.execute(Command::Delete(f.nodes[0])).unwrap(); + assert!(doc.project().contains(f.work[0])); + assert!(doc.project().contains(f.exchange)); + assert!(doc.project().contains(f.nodes[1])); + assert!(!doc.project().contains(f.link)); + doc.undo(); + assert_eq!(doc.bundle(), &reconnected); + doc.undo(); + assert_eq!(doc.bundle(), &original); + let reordered = execute_batch( + &f.p, + &[direct::reorder(&f.p, f.owner, f.nodes[2], f.nodes[0]).unwrap()], + ) + .unwrap(); + assert_eq!(reordered.elements[&f.owner].steps().unwrap()[0], f.nodes[2]); + assert_eq!(reordered.relations, f.p.relations); + assert!(direct::reorder(&f.p, f.owner, f.nodes[0], f.nodes[1]).is_err()); +} +#[test] +fn direct_process_reuse_preview_is_local_expandable_and_recursive_sources_are_rejected() { + let mut f = fixture(Layer::Oa); + let source = + f.p.make_element(Kind::Process, Layer::Oa, "Reusable", None) + .unwrap(); + let source_id = source.id; + f.p = execute_batch(&f.p, &[Command::CreateElement(source)]).unwrap(); + for i in 0..8 { + let e = occurrence( + &f.p, + source_id, + &format!("Shared step {i}"), + OccurrenceBody::Action { + action: Some(f.work[0]), + lifeline: None, + }, + ); + f.p = execute_batch(&f.p, &[Command::CreateElement(e)]).unwrap(); + } + let shared = f.p.clone(); + let mut references = Vec::new(); + for _ in 0..2 { + let plan = command(&f, Tool::Reuse, Some(source_id), Insertion::default()); + let result = execute_batch(&f.p, &[plan]).unwrap(); + references.push(created(&f.p, &result, f.owner)); + f.p = result; + } + let graph = Graph::new(&f.p, f.owner, references[0]); + let node = graph.nodes.iter().find(|n| n.id == references[0]).unwrap(); + assert_eq!(node.preview.len(), 4); + assert_eq!(node.preview_total, 8); + let mut input = Input::default(); + let point = Point::new(node.rect.x + 20., node.rect.y + 140.); + let clicked = send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + Rectangle::new(Point::ORIGIN, graph.size), + ); + assert!( + matches!(clicked,Some(Message::Select(id))if id==references[0]), + "preview click must select the local reference" + ); + let expanded = Graph::expanded( + &f.p, + f.owner, + references[0], + &BTreeSet::from([references[0]]), + ); + assert_eq!( + expanded + .nodes + .iter() + .find(|n| n.id == references[0]) + .unwrap() + .preview + .len(), + 8 + ); + assert_eq!( + expanded + .nodes + .iter() + .find(|n| n.id == references[1]) + .unwrap() + .preview + .len(), + 4 + ); + for e in shared.children(source_id) { + assert_eq!(f.p.elements[&e.id], *e); + } + assert!( + !targets(&f.p, source_id, Tool::Reuse) + .iter() + .any(|c| c.id == f.owner) + ); + let inside = f.p.elements[&source_id].steps().unwrap()[0]; + assert!(!validation::can_connect( + &f.p, + RelationKind::SequenceLink, + references[0], + inside + )); +} +#[test] +fn direct_process_nested_source_reopen_and_api_command_use_one_transaction() { + let bundle=SourceBundle{entry:"project.arcadia".into(),files:[ + ("project.arcadia".into(),"arcadia 4; project rescue \"Rescue\" { include \"oa/work.arcadia\"; include \"oa/process.arcadia\"; }\n".into()), + ("oa/work.arcadia".into(),"// Shared work\noa_activity assess \"Assess\" {}\n".into()), + ("oa/process.arcadia".into(),"// Contextual process\noa_process respond \"Respond\" {}\n".into()), + ].into()}; + let mut doc = Document::from_bundle(bundle, ProjectLimits::default()).unwrap(); + let original = doc.bundle().clone(); + let owner = doc + .project() + .elements + .values() + .find(|e| e.alias == "respond") + .unwrap() + .id; + let work = doc + .project() + .elements + .values() + .find(|e| e.alias == "assess") + .unwrap() + .id; + let mut state = State::default(); + state + .begin( + doc.project(), + context(owner), + Tool::Work, + ControlKind::Choice, + Insertion::default(), + ) + .unwrap(); + state + .update( + doc.project(), + Event::Target(Choice { + id: work, + label: "Assess".into(), + }), + ) + .unwrap(); + let plan = state.prepare_pending(doc.project()).unwrap(); + let expected = execute_batch( + doc.project(), + &[serde_json::from_value(serde_json::to_value(&plan).unwrap()).unwrap()], + ) + .unwrap(); + doc.execute(plan).unwrap(); + let id = doc.project().elements[&owner].steps().unwrap()[0]; + assert_eq!( + doc.compiled().index.declarations[&id].path, + "oa/process.arcadia" + ); + assert_eq!( + doc.bundle().files["oa/work.arcadia"], + original.files["oa/work.arcadia"] + ); + assert_eq!(doc.project(), &expected); + let after = doc.bundle().clone(); + doc.undo(); + assert_eq!(doc.bundle(), &original); + doc.redo(); + assert_eq!(doc.bundle(), &after); + let reopened = Document::from_bundle(after, ProjectLimits::default()).unwrap(); + assert_eq!(reopened.project(), &expected); +} +fn graph(f: &Fixture, tool: CanvasTool) -> Graph<'_> { + let mut graph = Graph::new(&f.p, f.owner, f.owner); + graph.interaction = Some((context(f.owner), tool, true)); + graph +} +fn send( + graph: &Graph<'_>, + input: &mut Input, + event: mouse::Event, + point: Point, + bounds: Rectangle, +) -> Option { + canvas::Program::update( + graph, + input, + &canvas::Event::Mouse(event), + bounds, + mouse::Cursor::Available(point + graph.scroll_offset), + ) + .and_then(|a| a.into_inner().0) +} +fn position(graph: &Graph<'_>, id: Id) -> Point { + graph + .nodes + .iter() + .find(|n| n.id == id) + .unwrap() + .rect + .center() +} +#[test] +fn direct_process_production_click_click_drag_insert_and_scroll_coordinates() { + let f = fixture(Layer::Oa); + for drag in [false, true] { + let mut graph = graph(&f, CanvasTool::Connect); + graph.scroll_offset = iced::Vector::new(35., 90.); + let bounds = Rectangle::new(Point::new(10., 20.), graph.size); + let screen = + |id| position(&graph, id) + iced::Vector::new(bounds.x, bounds.y) - graph.scroll_offset; + let a = screen(f.nodes[0]); + let b = screen(f.nodes[2]); + let mut input = Input::default(); + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: a }, + a, + bounds, + ); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + if !drag { + assert!( + send( + &graph, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + a, + bounds + ) + .is_none() + ); + } + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + if !drag { + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + b, + bounds, + ); + } + assert!( + matches!(send(&graph,&mut input,mouse::Event::ButtonReleased(mouse::Button::Left),b,bounds),Some(Message::TopologyCanvas(CanvasEvent::Connect{source,target,..}))if source==f.nodes[0]&&target==f.nodes[2]) + ); + assert!( + send( + &graph, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + bounds + ) + .is_none() + ); + } + let graph = graph(&f, CanvasTool::Add(Tool::Exchange, ControlKind::Choice)); + let bounds = Rectangle::new(Point::ORIGIN, graph.size); + let e = &graph.edges[0]; + let point = Point::new((e.points[0].x + e.points[1].x) / 2., e.points[0].y); + let mut input = Input::default(); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + bounds, + ); + assert!( + matches!(send(&graph,&mut input,mouse::Event::ButtonReleased(mouse::Button::Left),point,bounds),Some(Message::TopologyCanvas(CanvasEvent::Begin{insertion:Insertion{link:Some(id),..},..}))if id==f.link) + ); +} +#[test] +fn direct_process_production_stale_focus_scroll_bounds_and_pending_cancel() { + let f = fixture(Layer::Oa); + for change in 0..8 { + let mut graph = graph(&f, CanvasTool::Connect); + let bounds = Rectangle::new(Point::ORIGIN, graph.size); + let a = position(&graph, f.nodes[0]); + let b = position(&graph, f.nodes[2]); + let mut input = Input::default(); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + let mut end_bounds = bounds; + match change { + 0 => graph.interaction.as_mut().unwrap().0.epoch += 1, + 1 => graph.interaction.as_mut().unwrap().2 = false, + 2 => end_bounds.y -= 40., + 3 => { + canvas::Program::update( + &graph, + &mut input, + &canvas::Event::Window(iced::window::Event::Unfocused), + bounds, + mouse::Cursor::Available(b), + ); + } + 4 => { + send( + &graph, + &mut input, + mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { x: 0., y: 1. }, + }, + b, + bounds, + ); + } + 5 => graph.interaction.as_mut().unwrap().0.document = Id::new_v4(), + 6 => graph.interaction.as_mut().unwrap().1 = CanvasTool::Select, + _ => { + canvas::Program::update( + &graph, + &mut input, + &canvas::Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape), + modified_key: iced::keyboard::Key::Named( + iced::keyboard::key::Named::Escape, + ), + physical_key: iced::keyboard::key::Physical::Code( + iced::keyboard::key::Code::Escape, + ), + location: iced::keyboard::Location::Standard, + modifiers: iced::keyboard::Modifiers::empty(), + text: None, + repeat: false, + }), + bounds, + mouse::Cursor::Available(b), + ); + } + } + assert!( + send( + &graph, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + b, + end_bounds + ) + .is_none(), + "change {change}" + ); + } +} +#[test] +fn direct_process_readable_default_fit_expansion_and_bounded_choices() { + let f = fixture(Layer::Oa); + let mut state = State::default(); + state.enter(Id::new_v4(), f.owner); + state.viewport(Size::new(500., 400.)); + assert_eq!(state.scale(Size::new(3262., 220.), 500.), 1.); + state.zoom(None); + assert!(state.scale(Size::new(3262., 220.), 500.) < 0.2); + state.zoom(Some(0.1)); + assert_eq!(state.scale(Size::new(3262., 220.), 500.), 0.1); + state.enter(Id::new_v4(), f.owner); + assert_eq!(state.scale(Size::new(3262., 220.), 500.), 1.); + let mut all = Vec::new(); + for page in 0..500usize.div_ceil(24) { + let range = view::choice_page(500, page); + assert!(range.len() <= 24); + all.extend(range); + } + assert_eq!(all, (0..500).collect::>()); + assert_eq!(view::choice_page(0, 90), 0..0); +} + +#[test] +fn direct_process_production_endpoint_and_peer_reorder_handles_publish_exact_identity() { + let f = fixture(Layer::Oa); + let mut graph = graph(&f, CanvasTool::Select); + let bounds = Rectangle::new(Point::ORIGIN, graph.size); + graph.selected = f.link; + let mut input = Input::default(); + let a = *graph + .edges + .iter() + .find(|e| e.id == f.link) + .unwrap() + .points + .last() + .unwrap(); + let b = position(&graph, f.nodes[2]); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + assert!( + matches!(send(&graph,&mut input,mouse::Event::ButtonReleased(mouse::Button::Left),b,bounds),Some(Message::TopologyCanvas(CanvasEvent::Reconnect{link,end:direct::End::Target,target,..}))if link==f.link&&target==f.nodes[2]) + ); + graph.selected = f.nodes[2]; + let n = graph.nodes.iter().find(|n| n.id == f.nodes[2]).unwrap(); + let a = Point::new(n.rect.x + n.rect.width - 20., n.rect.y + 18.); + let b = position(&graph, f.nodes[0]); + let mut input = Input::default(); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + assert!( + matches!(send(&graph,&mut input,mouse::Event::ButtonReleased(mouse::Button::Left),b,bounds),Some(Message::TopologyCanvas(CanvasEvent::Reorder{occurrence,before,..}))if occurrence==f.nodes[2]&&before==f.nodes[0]) + ); +} +#[test] +fn direct_process_real_picker_stays_bounded_and_search_keeps_selection() { + let f = fixture(Layer::Oa); + let mut state = State::default(); + state + .begin( + &f.p, + context(f.owner), + Tool::Work, + ControlKind::Choice, + Insertion::default(), + ) + .unwrap(); + state + .update( + &f.p, + Event::Target(Choice { + id: f.work[0], + label: "Keep".into(), + }), + ) + .unwrap(); + let mut choices: Vec<_> = (0..500) + .map(|i| Choice { + id: Id::new_v4(), + label: format!("Activity {i}"), + }) + .collect(); + choices.push(Choice { + id: f.work[0], + label: "Selected source".into(), + }); + fn nodes(tree: &iced::advanced::widget::Tree) -> usize { + 1 + tree.children.iter().map(nodes).sum::() + } + for page in 0..21 { + state.update(&f.p, Event::Page(0, page)).unwrap(); + let d = state.draft.as_ref().unwrap(); + let view = view::picker(choices.clone(), d, 0, d.target, "Choose", Event::Target); + assert!(nodes(&iced::advanced::widget::Tree::new(view.as_widget())) < 40); + } + state + .update(&f.p, Event::Search(0, "Activity 499".into())) + .unwrap(); + assert_eq!(state.draft.as_ref().unwrap().target, Some(f.work[0])); + assert_eq!(state.draft.as_ref().unwrap().browsers[0].page, 0); +} + +pub(crate) fn empty_exchange_fixture(layer: Layer) -> (Fixture, Id) { + let mut f = fixture(layer); + let mut commands = f.nodes.into_iter().map(Command::Delete).collect::>(); + commands.push(Command::Connect( + f.p.relations[&f.exchange].kind(), + "Response".into(), + f.work[1], + f.work[2], + )); + f.p = execute_batch(&f.p, &commands).unwrap(); + let next = + f.p.relations + .values() + .find(|r| r.name == "Response") + .unwrap() + .id; + assert!(f.p.elements[&f.owner].steps().unwrap().is_empty()); + (f, next) +} + +#[test] +fn direct_process_append_preserves_existing_order_and_rejects_incompatible_exchange() { + for layer in [Layer::Oa, Layer::Sa] { + let (mut f, next) = empty_exchange_fixture(layer); + let kind = f.p.relations[&f.exchange].kind(); + f.p = execute_batch( + &f.p, + &[Command::Connect( + kind, + "Recovery".into(), + f.work[2], + f.work[0], + )], + ) + .unwrap(); + let final_exchange = + f.p.relations + .values() + .find(|r| r.name == "Recovery") + .unwrap() + .id; + for exchange in [f.exchange, next] { + let e = occurrence( + &f.p, + f.owner, + "Ordered contribution", + OccurrenceBody::Exchange { + operation: None, + exchange: Some(exchange), + sender: None, + receiver: None, + message_kind: MessageKind::default(), + reply_to: None, + values: vec![], + }, + ); + f.p = execute_batch(&f.p, &[Command::CreateElement(e)]).unwrap(); + } + let before = f.p.elements[&f.owner].steps().unwrap().clone(); + assert_eq!(legacy_order(&f.p, f.owner), before); + assert!(sequence_links(&f.p, f.owner).is_empty()); + let mut state = State::default(); + state.append_exchange(&f.p, f.owner).unwrap(); + let draft = state.draft.as_ref().unwrap(); + assert!(draft.fixed); + assert_eq!(draft.before, before.last().copied()); + assert_eq!(draft.after, None); + assert!(!compatible_source(&f.p, draft, f.exchange)); + assert!(compatible_source(&f.p, draft, final_exchange)); + state + .update( + &f.p, + Event::Target(Choice { + id: f.exchange, + label: "Incompatible".into(), + }), + ) + .unwrap(); + assert!(state.prepare_pending(&f.p).is_err()); + assert!(state.draft.is_some()); + state + .update( + &f.p, + Event::Target(Choice { + id: final_exchange, + label: "Recovery".into(), + }), + ) + .unwrap(); + let command = state.prepare_pending(&f.p).unwrap(); + let Command::Batch(commands) = &command else { + panic!("one ordinary batch") + }; + assert_eq!( + commands.len(), + 4, + "materialize old path + occurrence + append link + order" + ); + let serialized = serde_json::to_value(command).unwrap(); + let result = execute_batch(&f.p, &[serde_json::from_value(serialized).unwrap()]).unwrap(); + let after = result.elements[&f.owner].steps().unwrap(); + assert_eq!(&after[..2], &before); + assert_eq!(after.len(), 3); + let links = sequence_links(&result, f.owner); + assert_eq!(links.len(), 2); + for pair in after.windows(2) { + assert_eq!( + links + .iter() + .filter(|r| r.source() == pair[0] && r.target() == pair[1]) + .count(), + 1 + ); + } + assert!(uses_explicit_topology(&result, f.owner)); + for (id, relation) in &f.p.relations { + assert_eq!(result.relations[id], *relation); + } + } +} + +fn assert_interactive_owner_view(p: &Project, owner: Id) { + use iced::advanced::widget::Tree; + fn contains(tree: &Tree, tag: iced::advanced::widget::tree::Tag) -> bool { + tree.tag == tag || tree.children.iter().any(|child| contains(child, tag)) + } + let mut app = App::with_project(p.clone()); + app.layer = p.elements[&owner].layer(); + app.page = crate::app::Page::Behavior; + app.selected = owner; + app.behavior.active_owner = Some(owner); + let graph: Element<'_, Message> = canvas::Canvas::new(Graph::new(p, owner, owner)).into(); + let tag = Tree::new(graph.as_widget()).tag; + let view = view::view(&app, owner, Column::new()); + assert!( + contains(&Tree::new(view.as_widget()), tag), + "every path uses the interactive graph" + ); +} + +#[test] +fn direct_process_three_ordered_exchanges_preserve_outer_paths_on_insertion() { + for layer in [Layer::Oa, Layer::Sa] { + let (mut f, next) = empty_exchange_fixture(layer); + f.p = execute_batch( + &f.p, + &[Command::Connect( + f.p.relations[&f.exchange].kind(), + "Return for reassessment".into(), + f.work[2], + f.work[0], + )], + ) + .unwrap(); + let last = + f.p.relations + .values() + .find(|r| r.name == "Return for reassessment") + .unwrap() + .id; + for exchange in [f.exchange, next, last] { + let node = occurrence( + &f.p, + f.owner, + "Ordered contribution", + OccurrenceBody::Exchange { + operation: None, + exchange: Some(exchange), + sender: None, + receiver: None, + message_kind: MessageKind::default(), + reply_to: None, + values: vec![], + }, + ); + f.p = execute_batch(&f.p, &[Command::CreateElement(node)]).unwrap(); + } + for index in [0, 2] { + let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); + let before = doc.bundle().clone(); + let old_order = legacy_order(doc.project(), f.owner); + let graph = Graph::new(doc.project(), f.owner, f.owner); + assert_eq!(graph.ordered_paths.len(), 2); + assert!(graph.edges.is_empty()); + let mut state = State::default(); + state + .begin( + doc.project(), + context(f.owner), + Tool::Work, + ControlKind::Choice, + Insertion { + link: None, + before: Some(old_order[index]), + }, + ) + .unwrap(); + state + .update( + doc.project(), + Event::Target(Choice { + id: f.work[index], + label: "Shared activity".into(), + }), + ) + .unwrap(); + doc.execute(state.prepare_pending(doc.project()).unwrap()) + .unwrap(); + let added = created(&f.p, doc.project(), f.owner); + let mut expected = old_order; + expected.insert(index, added); + assert_eq!(doc.project().elements[&f.owner].steps().unwrap(), &expected); + let links = sequence_links(doc.project(), f.owner); + assert_eq!(links.len(), 3); + for pair in expected.windows(2) { + assert_eq!( + links + .iter() + .filter(|r| r.source() == pair[0] && r.target() == pair[1]) + .count(), + 1 + ); + } + for (id, relation) in &f.p.relations { + assert_eq!(&doc.project().relations[id], relation); + } + let after = doc.bundle().clone(); + assert!(doc.undo()); + assert_eq!(doc.bundle(), &before); + assert_eq!(doc.project(), &f.p); + assert!(doc.redo()); + assert_eq!(doc.bundle(), &after); + } + } +} + +#[test] +fn direct_process_incompatible_imported_order_rejects_conversion_without_losing_draft() { + for layer in [Layer::Oa, Layer::Sa] { + let (mut f, _) = empty_exchange_fixture(layer); + // Repeated exchange uses have different occurrences, but this legacy path + // has an endpoint gap. Converting its implied adjacency must reject. + for _ in 0..2 { + let node = occurrence( + &f.p, + f.owner, + "Repeated exchange", + OccurrenceBody::Exchange { + operation: None, + exchange: Some(f.exchange), + sender: None, + receiver: None, + message_kind: MessageKind::default(), + reply_to: None, + values: vec![], + }, + ); + f.p = execute_batch(&f.p, &[Command::CreateElement(node)]).unwrap(); + } + let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); + let before = doc.bundle().clone(); + assert_interactive_owner_view(doc.project(), f.owner); + assert_eq!( + Graph::new(doc.project(), f.owner, f.owner) + .ordered_paths + .len(), + 1 + ); + let mut state = State::default(); + state + .begin( + doc.project(), + context(f.owner), + Tool::Control, + ControlKind::Choice, + Insertion::default(), + ) + .unwrap(); + state + .update( + doc.project(), + Event::Name("Retain this proposed decision".into()), + ) + .unwrap(); + assert!(state.prepare_pending(doc.project()).is_err()); + assert_eq!( + state.draft.as_ref().unwrap().name, + "Retain this proposed decision" + ); + assert_eq!(doc.bundle(), &before); + assert_eq!(doc.project(), &f.p); + assert!(!doc.undo(), "rejection must not create history"); + state.update(doc.project(), Event::Cancel).unwrap(); + assert!(state.draft.is_none()); + assert_eq!(doc.bundle(), &before); + } +} + +#[test] +fn direct_process_empty_first_and_second_exchange_keep_the_same_interactive_renderer() { + for layer in [Layer::Oa, Layer::Sa] { + let (f, next) = empty_exchange_fixture(layer); + let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); + for (index, target) in [f.exchange, next].into_iter().enumerate() { + assert_interactive_owner_view(doc.project(), f.owner); + let before = doc.bundle().clone(); + let mut graph = Graph::new(doc.project(), f.owner, f.owner); + let c = context(f.owner); + graph.interaction = Some(( + c, + CanvasTool::Add(Tool::Exchange, ControlKind::Choice), + true, + )); + let mut input = Input::default(); + let bounds = Rectangle::with_size(graph.size); + let point = Point::new(graph.size.width - 10., graph.size.height - 10.); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + bounds, + ); + let Some(Message::TopologyCanvas(CanvasEvent::Begin { insertion, .. })) = send( + &graph, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + point, + bounds, + ) else { + panic!("canvas retains direct exchange insertion"); + }; + let mut state = State::default(); + state + .begin( + doc.project(), + c, + Tool::Exchange, + ControlKind::Choice, + insertion, + ) + .unwrap(); + state + .update( + doc.project(), + Event::Target(Choice { + id: target, + label: "Exchange".into(), + }), + ) + .unwrap(); + doc.execute(state.prepare_pending(doc.project()).unwrap()) + .unwrap(); + assert_eq!( + doc.project().elements[&f.owner].steps().unwrap().len(), + index + 1 + ); + assert_eq!(sequence_links(doc.project(), f.owner).len(), index); + assert_interactive_owner_view(doc.project(), f.owner); + let after = doc.bundle().clone(); + assert!(doc.undo()); + assert_eq!(doc.bundle(), &before); + assert!(doc.redo()); + assert_eq!(doc.bundle(), &after); + } + } +} + +#[test] +fn direct_process_imported_order_is_projected_without_mutation_and_first_canvas_edit_is_atomic() { + for layer in [Layer::Oa, Layer::Sa] { + let (mut f, next) = empty_exchange_fixture(layer); + for exchange in [f.exchange, next] { + let node = occurrence( + &f.p, + f.owner, + "Imported exchange", + OccurrenceBody::Exchange { + operation: None, + exchange: Some(exchange), + sender: None, + receiver: None, + message_kind: MessageKind::default(), + reply_to: None, + values: vec![], + }, + ); + f.p = execute_batch(&f.p, &[Command::CreateElement(node)]).unwrap(); + } + let mut doc = Document::from_project(f.p.clone(), ProjectLimits::default()).unwrap(); + let before = doc.bundle().clone(); + let order = legacy_order(doc.project(), f.owner); + assert_eq!(order.len(), 2); + assert_interactive_owner_view(doc.project(), f.owner); + let mut graph = Graph::new(doc.project(), f.owner, order[0]); + assert!( + graph.edges.is_empty(), + "projection never invents relation identities" + ); + assert_eq!(graph.ordered_paths.len(), 1); + assert!(position(&graph, order[0]).x < position(&graph, order[1]).x); + let points = &graph.ordered_paths[0].points; + let point = Point::new((points[0].x + points[1].x) / 2., points[0].y); + let c = context(f.owner); + graph.interaction = Some((c, CanvasTool::Add(Tool::Work, ControlKind::Choice), true)); + let mut input = Input::default(); + let bounds = Rectangle::with_size(graph.size); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + bounds, + ); + let Some(Message::TopologyCanvas(CanvasEvent::Begin { insertion, .. })) = send( + &graph, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + point, + bounds, + ) else { + panic!("ordered arrow supports direct insertion"); + }; + assert_eq!( + insertion, + Insertion { + link: None, + before: Some(order[1]) + } + ); + assert_eq!(doc.bundle(), &before); + let mut state = State::default(); + state + .begin(doc.project(), c, Tool::Work, ControlKind::Choice, insertion) + .unwrap(); + state + .update( + doc.project(), + Event::Target(Choice { + id: f.work[1], + label: "Respond".into(), + }), + ) + .unwrap(); + doc.execute(state.prepare_pending(doc.project()).unwrap()) + .unwrap(); + let inserted = created(&f.p, doc.project(), f.owner); + assert_eq!( + doc.project().elements[&f.owner].steps().unwrap(), + &[order[0], inserted, order[1]] + ); + let links = sequence_links(doc.project(), f.owner); + assert_eq!(links.len(), 2); + for (a, b) in [(order[0], inserted), (inserted, order[1])] { + assert!( + links + .iter() + .any(|link| link.source() == a && link.target() == b) + ); + } + assert!( + Graph::new(doc.project(), f.owner, inserted) + .ordered_paths + .is_empty() + ); + for id in f.work { + assert_eq!(doc.project().elements[&id], f.p.elements[&id]); + } + assert!(doc.undo()); + assert_eq!(doc.bundle(), &before); + assert_eq!(doc.project(), &f.p); + } +} + +#[test] +fn direct_process_palette_preserves_fit_and_manual_camera() { + for layer in [Layer::Oa, Layer::Sa] { + let f = fixture(layer); + let mut state = State::default(); + let document = Id::new_v4(); + state.enter(document, f.owner); + state.viewport(Size::new(500., 300.)); + state.scrolled(scenario::ScrollArea::Canvas, iced::Vector::new(35., 90.)); + state.scrolled(scenario::ScrollArea::Page, iced::Vector::new(0., 120.)); + for camera in [None, Some(0.75), Some(1.5)] { + state.zoom(camera); + let size = Size::new(690., 250.); + let before = state.scale(size, 500.); + for tool in [ + CanvasTool::Add(Tool::Work, ControlKind::Choice), + CanvasTool::Add(Tool::Exchange, ControlKind::Choice), + CanvasTool::Add(Tool::Reuse, ControlKind::Choice), + CanvasTool::Add(Tool::Control, ControlKind::Choice), + CanvasTool::Add(Tool::Control, ControlKind::Fork), + CanvasTool::Connect, + CanvasTool::Select, + ] { + state.choose(tool); + assert_eq!( + state.scale(size, 500.), + before, + "{layer:?} {camera:?} {tool:?}" + ); + assert_eq!(state.fit, camera.is_none()); + assert_eq!(state.scroll_offset(), iced::Vector::new(35., 210.)); + state.enter(document, f.owner); + assert_eq!(state.scale(size, 500.), before); + } + } + state.enter(Id::new_v4(), f.owner); + assert_eq!(state.scale(Size::new(690., 250.), 500.), 1.); + assert_eq!(state.scroll_offset(), iced::Vector::ZERO); + } +} + +#[test] +fn direct_process_scaled_control_and_link_gestures_use_visible_positions() { + for layer in [Layer::Oa, Layer::Sa] { + let f = fixture(layer); + for scale in [0.15, 0.6898551, 1.5] { + let mut graph = graph(&f, CanvasTool::Add(Tool::Control, ControlKind::Choice)); + graph.scale = scale; + graph.scroll_offset = iced::Vector::new(11., 19.); + let bounds = Rectangle::new( + Point::new(40., 70.), + Size::new(graph.size.width * scale, graph.size.height * scale), + ); + let screen = |world: Point| { + Point::new( + world.x * scale + bounds.x - 11., + world.y * scale + bounds.y - 19., + ) + }; + let edge = &graph.edges[0]; + let point = screen(Point::new( + (edge.points[0].x + edge.points[1].x) / 2., + edge.points[0].y, + )); + let mut input = Input::default(); + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: point }, + point, + bounds, + ); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + point, + bounds, + ); + assert!( + matches!( + send(&graph, &mut input, mouse::Event::ButtonReleased(mouse::Button::Left), point, bounds), + Some(Message::TopologyCanvas(CanvasEvent::Begin { + tool: Tool::Control, control: ControlKind::Choice, + insertion: Insertion { link: Some(id), .. }, .. + })) if id == f.link + ), + "{layer:?} control at {scale}" + ); + graph.interaction.as_mut().unwrap().1 = CanvasTool::Connect; + for drag in [false, true] { + let mut input = Input::default(); + let a = screen(position(&graph, f.nodes[0])); + let b = screen(position(&graph, f.nodes[2])); + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: a }, + a, + bounds, + ); + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + a, + bounds, + ); + if !drag { + send( + &graph, + &mut input, + mouse::Event::ButtonReleased(mouse::Button::Left), + a, + bounds, + ); + } + send( + &graph, + &mut input, + mouse::Event::CursorMoved { position: b }, + b, + bounds, + ); + if !drag { + send( + &graph, + &mut input, + mouse::Event::ButtonPressed(mouse::Button::Left), + b, + bounds, + ); + } + assert!( + matches!( + send(&graph, &mut input, mouse::Event::ButtonReleased(mouse::Button::Left), b, bounds), + Some(Message::TopologyCanvas(CanvasEvent::Connect { source, target, .. })) + if source == f.nodes[0] && target == f.nodes[2] + ), + "{layer:?} link at {scale}, drag={drag}" + ); + } + } + } +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/layout_tests.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/layout_tests.rs new file mode 100644 index 00000000..add885b5 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/layout_tests.rs @@ -0,0 +1,887 @@ +use super::*; + +#[tokio::test] +async fn production_process_canvas_scaled_path_creation_preserves_source_and_editor_bounds() { + use crate::behavior::canvas_workspace::{self, tests::Mounted}; + use iced::Vector; + use iced::advanced::widget; + let initial = super::super::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(initial.p); + let _ = app.update(Message::Select(initial.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + let mut mounted = Mounted::new(&mut app).await; + for layer in [Layer::Oa, Layer::Sa] { + for size in [Size::new(1100., 700.), Size::new(1440., 900.)] { + let f = super::super::direct_tests::fixture(layer); + app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = size; + mounted.settle(&mut app).await; + for scale in [0.5, 1., 2.] { + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Zoom( + context, + Some(scale), + ))); + let geometry = mounted.geometry(&app); + let bounds = geometry.bounds("process-viewport"); + assert!(bounds.height > 350.); + assert!( + !geometry + .scrolls + .contains_key(&widget::Id::new("process-canvas")) + ); + let graph = Graph::new(app.project(), f.owner, f.owner); + let edge = graph.edges.iter().find(|edge| edge.id == f.link).unwrap(); + let segment = edge + .points + .windows(2) + .max_by(|a, b| a[0].distance(a[1]).total_cmp(&b[0].distance(b[1]))) + .unwrap(); + let world = Point::new( + (segment[0].x + segment[1].x) / 2., + (segment[0].y + segment[1].y) / 2., + ); + let desired = Vector::new(85., 130.); + let next_pan = desired - Vector::new(world.x, world.y) * scale; + let delta = next_pan - app.behavior.topology.pan; + let start = bounds.position() + Vector::new(130., 70.); + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + mounted.pan(&mut app, start, start + delta).await; + let diff = app.behavior.topology.pan - next_pan; + assert!(diff.x.hypot(diff.y) < 0.01); + let tool = mounted.geometry(&app).bounds("behavior-tool:OR").center(); + mounted.click(&mut app, tool).await; + let messages = mounted.click(&mut app, bounds.position() + desired).await; + assert!( + messages.iter().any( + |m| matches!(m, Message::TopologyCanvas(CanvasEvent::Begin { + tool: Tool::Control, control: ControlKind::Choice, insertion, .. + }) if insertion.link == Some(f.link) && insertion.before.is_none()) + ), + "{layer:?} {scale}: {messages:?}" + ); + let invocation = app.behavior.topology.draft.as_ref().unwrap().invocation; + let _ = app.update(event(Event::Name(" raw choice 雨 ".into()))); + let raw = app.behavior.recovery_snapshot().unwrap(); + mounted.settle(&mut app).await; + let shown = mounted.geometry(&app); + assert_eq!(shown.bounds("process-viewport"), bounds); + let actions = shown.bounds("process-actions"); + let form = shown.scrolls[&widget::Id::new("process-form")].0; + assert!(actions.y >= bounds.y && actions.y + actions.height <= form.y); + assert!(form.y + form.height <= bounds.y + bounds.height && form.height > 250.); + let pan = app.behavior.topology.pan; + mounted + .batch( + &mut app, + &[canvas_workspace::tests::key( + iced::keyboard::key::Named::ArrowRight, + )], + mouse::Cursor::Available(bounds.position() + desired), + ) + .await; + assert_eq!(app.behavior.topology.pan, pan); + mounted.scroll(&app, "process-form", 500.); + assert_eq!(mounted.geometry(&app).bounds("process-actions"), actions); + for shelf in [Shelf::Tools, Shelf::Selection, Shelf::Selection] { + let _ = app.update(event(Event::Shelf(shelf))); + assert_eq!(mounted.geometry(&app).bounds("process-viewport"), bounds); + assert_eq!( + app.behavior.topology.draft.as_ref().unwrap().invocation, + invocation + ); + assert_eq!(app.behavior.recovery_snapshot().unwrap(), raw); + } + let _ = app.update(event(Event::Cancel)); + assert_eq!(mounted.geometry(&app).bounds("process-viewport"), bounds); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); + app.behavior.topology.shelf = None; + } + } + } +} + +#[tokio::test] +async fn production_process_canvas_focus_wheel_fit_and_stale_camera_are_presentation_only() { + use crate::behavior::canvas_workspace::{ + Event as Camera, + tests::{Mounted, key}, + }; + use iced::Vector; + let f = super::super::direct_tests::fixture(Layer::Sa); + let mut app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let bounds = mounted.geometry(&app).bounds("process-viewport"); + let point = bounds.position() + Vector::new(90., 120.); + let accepted = app.document.snapshot(); + let bundle = app.document.bundle().clone(); + mounted + .pan(&mut app, point, point + Vector::new(-20., 25.)) + .await; + let pan = app.behavior.topology.pan; + mounted + .batch( + &mut app, + &[key(iced::keyboard::key::Named::ArrowRight)], + mouse::Cursor::Available(point), + ) + .await; + assert_eq!(app.behavior.topology.pan, pan + Vector::new(-60., 0.)); + let local = point - bounds.position(); + let pan = app.behavior.topology.pan; + let old_scale = app.behavior.topology.zoom.unwrap_or(1.); + let before_world = (local - pan) / old_scale; + mounted + .batch( + &mut app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { x: 0., y: 1. }, + }), + ], + mouse::Cursor::Available(point), + ) + .await; + let after_world = (local - app.behavior.topology.pan) / app.behavior.topology.zoom.unwrap(); + let diff = after_world - before_world; + assert!(diff.x.hypot(diff.y) < 0.001); + let context = app.topology_context().unwrap(); + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Zoom(context, None))); + let fitted = (app.behavior.topology.pan, app.behavior.topology.zoom); + let _ = app.update(event(Event::Shelf(Shelf::Selection))); + mounted.settle(&mut app).await; + app.window_size = Size::new(1440., 900.); + mounted.settle(&mut app).await; + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + fitted, + "Fit is a one-time camera action" + ); + for mismatch in 0..3 { + let mut stale = app.topology_context().unwrap(); + match mismatch { + 0 => stale.document = Id::new_v4(), + 1 => stale.owner = Id::new_v4(), + _ => stale.revision += 1, + } + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera( + stale, + app.camera_epoch, + Camera::Pan(Vector::new(400., 400.)), + ))); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + fitted + ); + } + app.hosted.visible = true; + let _ = app.update(Message::TopologyCanvas(CanvasEvent::Camera( + app.topology_context().unwrap(), + app.camera_epoch, + Camera::Pan(Vector::new(30., 40.)), + ))); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + fitted + ); + assert!(std::sync::Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &bundle); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_process_camera_external_controls_bounds_and_retained_deltas_cancel_old_drags() { + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + for boundary in ["Fit", "OR", "Details", "Panels", "Bounds"] { + let toolbar = capture_toolbar(&mut mounted, &app, "Fit"); + let bounds = mounted.geometry(&app).bounds("process-viewport"); + let point = bounds.position() + iced::Vector::new(95., 90.); + let lifetime = app.camera_epoch; + let pan = app.behavior.topology.pan; + let first = point + iced::Vector::new(13., 9.); + let last = first + iced::Vector::new(17., -3.); + let emitted = mounted + .batch( + &mut app, + &[ + iced::Event::Mouse(mouse::Event::CursorMoved { position: point }), + iced::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)), + iced::Event::Mouse(mouse::Event::CursorMoved { position: first }), + iced::Event::Mouse(mouse::Event::CursorMoved { position: last }), + ], + mouse::Cursor::Available(last), + ) + .await; + assert_eq!(app.behavior.topology.pan, pan + (last - point)); + assert_eq!( + app.camera_epoch, lifetime, + "own ordered deltas retain their lifetime" + ); + let retained = camera_delta(&emitted); + if boundary == "Bounds" { + let size = Size::new(app.window_size.width + 90., app.window_size.height + 35.); + let task = app.update(Message::WindowResized(size)); + mounted.perform(&mut app, task).await; + } else { + let id = scope(&app).control_id(boundary); + let at = mounted.geometry(&app).containers[&id].center(); + let clicked = mounted.click(&mut app, at).await; + assert!( + focused(&mut mounted, &app, boundary), + "same-batch pointer activation focuses the resulting live control without F6" + ); + if boundary == "Fit" { + mounted + .batch( + &mut app, + &[key(iced::keyboard::key::Named::Tab)], + mouse::Cursor::Unavailable, + ) + .await; + assert!( + focused(&mut mounted, &app, "ZoomOut"), + "Tab after the real press/release batch continues from Fit" + ); + assert!(clicked.iter().any(|m| matches!( + intent_action(m), + Message::TopologyCanvas(CanvasEvent::Zoom(_, None)) + ))); + assert_eq!( + app.behavior.topology.pan, + iced::Vector::ZERO, + "Fit emitted after this same batch's old pan must still execute" + ); + } + } + mounted.settle(&mut app).await; + assert_ne!( + app.camera_epoch, lifetime, + "{boundary} ends the old input lifetime" + ); + let camera = (app.behavior.topology.pan, app.behavior.topology.zoom); + let bounds = mounted.geometry(&app).bounds("process-viewport"); + let released = release_middle( + &mut mounted, + &mut app, + bounds.position() + iced::Vector::new(130., 145.), + ) + .await; + assert!(!released.iter().any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(_, _, crate::behavior::canvas_workspace::Event::Pan(delta))) if *delta != iced::Vector::ZERO)), + "{boundary} must cancel the retained widget drag"); + let task = app.update(retained); + mounted.perform(&mut app, task).await; + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera, + "{boundary} must reject its pre-boundary App delta" + ); + let current_lifetime = app.camera_epoch; + let task = app.update(toolbar); + mounted.perform(&mut app, task).await; + assert_eq!( + app.camera_epoch, current_lifetime, + "{boundary} rejects the retained toolbar lifetime" + ); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera, + "{boundary} retained Fit cannot alter the camera" + ); + } + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_process_keyboard_enters_activates_draws_focus_and_returns_to_inspector() { + use crate::behavior::canvas_workspace::tests::*; + use iced::keyboard::key::Named; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + assert!(!focused(&mut mounted, &app, "viewport")); + let operations = mounted.operations; + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!( + mounted.operations > operations, + "entry executes the returned App widget Task" + ); + assert!(focused(&mut mounted, &app, "viewport")); + let mut backwards = key(Named::Tab); + if let iced::Event::Keyboard(iced::keyboard::Event::KeyPressed { modifiers, .. }) = + &mut backwards + { + *modifiers = iced::keyboard::Modifiers::SHIFT; + } + mounted + .batch(&mut app, &[backwards], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "ActualSize")); + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "viewport")); + assert_eq!( + mounted + .geometry(&app) + .focused + .iter() + .filter(|(_, _, focused)| *focused) + .count(), + 1 + ); + tab_to(&mut mounted, &mut app, "OR").await; + let chosen = mounted + .batch(&mut app, &[key(Named::Enter)], mouse::Cursor::Unavailable) + .await; + assert!(chosen.iter().any(|m| matches!( + intent_action(m), + Message::TopologyCanvas(CanvasEvent::Choose(..)) + ))); + assert!( + focused(&mut mounted, &app, "OR"), + "tool activation retains its keyboard target" + ); + // The selected palette tool must still have a visibly distinct focus ring. + let tool_on = mounted.pixels(&app); + let geometry = mounted.geometry(&app); + let mut tool_bounds = geometry.containers[&scope(&app).control_id("OR")]; + let offset = geometry.scrolls[&scope(&app).control_id("palette-scroll")].1; + tool_bounds.x -= offset.x; + tool_bounds.y -= offset.y; + mounted + .batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + let tool_off = mounted.pixels(&app); + assert_drawn_focus(&tool_off, &tool_on, app.window_size, tool_bounds); + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + // Focus alone changes the visible border of the real production Fit control. + let before = mounted.pixels(&app); + tab_to(&mut mounted, &mut app, "Fit").await; + let id = scope(&app).control_id("Fit"); + let bounds = mounted.geometry(&app).containers[&id]; + let after = mounted.pixels(&app); + assert_drawn_focus(&before, &after, app.window_size, bounds); + let fitted = mounted + .batch(&mut app, &[key(Named::Space)], mouse::Cursor::Unavailable) + .await; + assert!(fitted.iter().any(|m| matches!( + intent_action(m), + Message::TopologyCanvas(CanvasEvent::Zoom(_, None)) + ))); + assert_eq!(app.behavior.topology.pan, iced::Vector::ZERO); + assert!(focused(&mut mounted, &app, "Fit")); + tab_to(&mut mounted, &mut app, "Canvas").await; + mounted + .batch(&mut app, &[key(Named::Enter)], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "viewport")); + mounted + .batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + assert!(!focused(&mut mounted, &app, "viewport")); + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + assert!( + mounted + .geometry(&app) + .focused + .iter() + .any(|(_, _, focused)| *focused), + "ordinary Tab resumes the production Inspector's own entry policy" + ); + assert!(!focused(&mut mounted, &app, "viewport")); + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!(focused(&mut mounted, &app, "viewport")); + assert_eq!( + mounted + .geometry(&app) + .focused + .iter() + .filter(|(_, _, focused)| *focused) + .count(), + 1, + "explicit canvas entry releases Inspector keyboard ownership" + ); + mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!(!focused(&mut mounted, &app, "viewport")); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + raw + ); + // Typing after exit belongs to Inspector. Replaying the prior clean tool + // action must retain those raw fields, never implicitly Apply them. + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + mounted + .batch(&mut app, &[character("x")], mouse::Cursor::Unavailable) + .await; + assert!(app.draft_dirty()); + let dirty_raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + assert_ne!(dirty_raw, raw); + let camera = (app.behavior.topology.pan, app.behavior.topology.zoom); + for message in chosen { + let task = app.update(message); + mounted.perform(&mut app, task).await; + } + let emitted = mounted + .batch(&mut app, &[key(Named::F6)], mouse::Cursor::Unavailable) + .await; + assert!( + !emitted + .iter() + .any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..)))) + ); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera + ); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + dirty_raw + ); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_process_caller_return_rejects_its_previous_camera_lifetime() { + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut project = f.p; + let target = project + .add_element(Kind::Process, Layer::Oa, "Reusable check", None) + .unwrap(); + let reference = occurrence( + &project, + f.owner, + "Local use", + OccurrenceBody::SequenceReference { + sequence: Some(target), + }, + ); + let use_id = reference.id; + project = execute_batch(&project, &[Command::CreateElement(reference)]).unwrap(); + let mut app = App::with_project(project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 700.); + let mut mounted = Mounted::new(&mut app).await; + let source = app.document.bundle().clone(); + let snapshot = app.document.snapshot(); + let bounds = mounted.geometry(&app).bounds("process-viewport"); + let point = bounds.position() + iced::Vector::new(80., 100.); + let emitted = mounted + .pan(&mut app, point, point + iced::Vector::new(35., -25.)) + .await; + let retained = camera_delta(&emitted); + let camera = (app.behavior.topology.pan, app.behavior.topology.zoom); + let toolbar = capture_toolbar(&mut mounted, &app, "Fit"); + let task = app.update(Message::TopologyCanvas(CanvasEvent::Open( + app.topology_context().unwrap(), + use_id, + ))); + mounted.perform(&mut app, task).await; + mounted.settle(&mut app).await; + assert_eq!(app.topology_context().unwrap().owner, target); + let target_bounds = mounted.geometry(&app).bounds("process-viewport"); + let point = target_bounds.position() + iced::Vector::new(70., 90.); + mounted + .pan(&mut app, point, point + iced::Vector::new(-50., 20.)) + .await; + let task = app.update(Message::TopologyCanvas(CanvasEvent::Back( + app.topology_context().unwrap(), + ))); + mounted.perform(&mut app, task).await; + mounted.settle(&mut app).await; + assert_eq!(app.topology_context().unwrap().owner, f.owner); + assert_eq!(app.selected, use_id); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera + ); + let task = app.update(retained); + mounted.perform(&mut app, task).await; + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera + ); + let lifetime = app.camera_epoch; + let task = app.update(toolbar); + mounted.perform(&mut app, task).await; + assert_eq!( + app.camera_epoch, lifetime, + "A to B and Back cannot revive toolbar intent" + ); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera + ); + mounted + .pan(&mut app, point, point + iced::Vector::new(12., -8.)) + .await; + assert_eq!( + app.behavior.topology.pan, + camera.0 + iced::Vector::new(12., -8.) + ); + assert_eq!(app.document.bundle(), &source); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_canvas_keyboard_and_delayed_tasks_respect_real_chooser_and_review_owners() { + use crate::behavior::canvas_workspace::tests::*; + use iced::keyboard::key::Named; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut project = f.p; + let target = project + .add_element(Kind::FunctionalChain, Layer::Sa, "Counterpart", None) + .unwrap(); + let trace = Id::new_v4(); + project.traces.insert( + trace, + Trace { + id: trace, + alias: "canvas_focus_trace".into(), + name: "Review camera ownership".into(), + description: String::new(), + oa: f.owner, + sa: target, + kind: TraceKind::Manual, + baseline: None, + }, + ); + let mut app = App::with_project(project); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1440., 900.); + let mut mounted = Mounted::new(&mut app).await; + let source = app.document.bundle().clone(); + let snapshot = app.document.snapshot(); + let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + let mut retained_apply = retained_focus_apply(&mut mounted, &mut app).await; + let entry = mounted.input(&app, &[key(Named::F6)], mouse::Cursor::Unavailable); + assert_eq!(entry.len(), 1); + let delayed = app.update(entry[0].clone()); + let draft = app.draft.as_ref().unwrap(); + let parent_id = + crate::forms::reference_chooser::trigger::field_id(draft.reference_generation(), "/parent"); + let parent_bounds = mounted + .geometry(&app) + .focused + .iter() + .find(|(id, _, _)| id.as_ref() == Some(&parent_id)) + .expect("actual Inspector reference trigger is mounted") + .1; + let request = draft.reference_open(app.project(), "/parent", parent_bounds); + let point = + mounted.geometry(&app).bounds("process-viewport").position() + iced::Vector::new(80., 100.); + press_middle(&mut mounted, &mut app, point).await; + let moved = point + iced::Vector::new(12., 8.); + let emitted = mounted + .batch( + &mut app, + &[iced::Event::Mouse(mouse::Event::CursorMoved { + position: moved, + })], + mouse::Cursor::Available(moved), + ) + .await; + let retained = camera_delta(&emitted); + let root_inputs = mounted.geometry(&app).text_inputs; + let task = app.update(Message::FieldReferenceOpen(request)); + mounted.perform(&mut app, task).await; + assert!( + app.reference_input_active(), + "the real App chooser must open" + ); + let popup_focus = mounted + .geometry(&app) + .focused + .into_iter() + .filter(|(_, _, value)| *value) + .collect::>(); + assert!( + popup_focus + .iter() + .any(|(id, bounds, focused)| id.is_none() && *focused && !root_inputs.contains(bounds)), + "the newly mounted nested chooser search, absent from root inputs, must be focused" + ); + mounted.perform(&mut app, delayed).await; + mounted.operate(&app, &mut *retained_apply); + assert_eq!( + mounted + .geometry(&app) + .focused + .into_iter() + .filter(|(_, _, value)| *value) + .collect::>(), + popup_focus, + "both delayed Collect and deliberately retained Apply respect the actual nested overlay owner" + ); + assert!(!focused(&mut mounted, &app, "viewport")); + let camera = (app.behavior.topology.pan, app.behavior.topology.zoom); + for key_name in [Named::F6, Named::Tab, Named::ArrowRight] { + let emitted = mounted + .batch(&mut app, &[key(key_name)], mouse::Cursor::Unavailable) + .await; + assert!( + !emitted + .iter() + .any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..)))) + ); + } + // Tab may dismiss the chooser to its owning field; Escape closes it if still open. + if app.reference_input_active() { + mounted + .batch(&mut app, &[key(Named::Escape)], mouse::Cursor::Unavailable) + .await; + } + assert!(!app.reference_input_active()); + let returned_focus = mounted + .geometry(&app) + .focused + .into_iter() + .filter(|(_, _, value)| *value) + .collect::>(); + mounted.operate(&app, &mut *retained_apply); + assert_eq!( + mounted + .geometry(&app) + .focused + .into_iter() + .filter(|(_, _, value)| *value) + .collect::>(), + returned_focus, + "the same owner after chooser Back has a different lifetime; retained Apply must stay inert" + ); + release_middle(&mut mounted, &mut app, point + iced::Vector::new(50., 35.)).await; + let task = app.update(retained); + mounted.perform(&mut app, task).await; + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera, + "a disabled-and-reenabled canvas cannot resume its pre-chooser pan" + ); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + raw + ); + + let mut retained_apply = retained_focus_apply(&mut mounted, &mut app).await; + let entry = mounted.input(&app, &[key(Named::F6)], mouse::Cursor::Unavailable); + let delayed = app.update( + entry + .into_iter() + .find(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..)))) + .unwrap(), + ); + let task = app.update(Message::StartSemanticReview(trace)); + mounted.perform(&mut app, task).await; + let facet = FacetSelector::Header(HeaderFacet::Parent); + for row in [ + crate::semantic_review::RowEvent::Select(facet.clone()), + crate::semantic_review::RowEvent::Decide(facet, Some(SemanticDecision::Candidate)), + ] { + let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else { + panic!("actual Review dialog opens"); + }; + let message = state.address_event(row); + let task = app.update(Message::Workflow(crate::workflow::Event::semantic(message))); + mounted.perform(&mut app, task).await; + } + let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else { + panic!("Review remains open"); + }; + let review = state.recovery_snapshot().unwrap(); + let review_id = crate::forms::reference_chooser::trigger::field_id( + state + .reference_form(app.project()) + .unwrap() + .reference_generation(), + "/parent", + ); + mounted + .batch(&mut app, &[key(Named::Tab)], mouse::Cursor::Unavailable) + .await; + assert!( + mounted + .geometry(&app) + .focused + .iter() + .any(|(id, _, focused)| id.as_ref() == Some(&review_id) && *focused) + ); + let scrolls = mounted.geometry(&app).scrolls; + mounted.perform(&mut app, delayed).await; + mounted.operate(&app, &mut *retained_apply); + assert_eq!( + mounted.geometry(&app).scrolls, + scrolls, + "a retained Apply must not scroll the new owner" + ); + for key_name in [Named::F6, Named::ArrowRight] { + let emitted = mounted + .batch(&mut app, &[key(key_name)], mouse::Cursor::Unavailable) + .await; + assert!( + !emitted + .iter() + .any(|m| matches!(m, Message::TopologyCanvas(CanvasEvent::Camera(..)))) + ); + } + assert!( + mounted + .geometry(&app) + .focused + .iter() + .any(|(id, _, focused)| id.as_ref() == Some(&review_id) && *focused) + ); + let Some(crate::workflow::Workflow::Semantic(state)) = app.workflow.as_ref() else { + panic!("Review retains ownership"); + }; + assert_eq!(state.recovery_snapshot().unwrap(), review); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera + ); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + raw + ); + assert_eq!(app.document.bundle(), &source); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_process_form_replacement_resets_scroll_without_a_closed_render() { + use crate::behavior::canvas_workspace::tests::Mounted; + use iced::advanced::widget; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 520.); + let mut mounted = Mounted::new(&mut app).await; + let source = app.document.bundle().clone(); + let snapshot = app.document.snapshot(); + let bounds = mounted.geometry(&app).bounds("process-viewport"); + let point = bounds.position() + iced::Vector::new(65., 80.); + mounted + .pan(&mut app, point, point + iced::Vector::new(-15., 24.)) + .await; + let camera = (app.behavior.topology.pan, app.behavior.topology.zoom); + let task = app.update(event(Event::Start(f.owner, Tool::Branch, None))); + mounted.perform(&mut app, task).await; + mounted.settle(&mut app).await; + let session = app.behavior.topology.draft.as_ref().unwrap().invocation; + mounted.scroll(&app, "process-form", 700.); + let offset = mounted.geometry(&app).scrolls[&widget::Id::new("process-form")].1; + assert!( + offset.y > 0., + "real production Branch fields must overflow this viewport" + ); + let task = app.update(event(Event::Name("Preserve this same session".into()))); + mounted.perform(&mut app, task).await; + assert_eq!( + mounted.geometry(&app).scrolls[&widget::Id::new("process-form")].1, + offset + ); + assert_eq!( + app.behavior.topology.draft.as_ref().unwrap().invocation, + session + ); + let task = app.update(event(Event::Cancel)); + mounted.perform(&mut app, task).await; + let task = app.update(event(Event::Start(f.owner, Tool::Branch, None))); + mounted.perform(&mut app, task).await; + assert_ne!( + app.behavior.topology.draft.as_ref().unwrap().invocation, + session + ); + // No closed-state App::view was rendered between Cancel and Start. + let geometry = mounted.geometry(&app); + assert_eq!( + geometry.scrolls[&widget::Id::new("process-form")].1, + iced::Vector::ZERO + ); + assert_eq!(geometry.bounds("process-viewport"), bounds); + assert_eq!( + (app.behavior.topology.pan, app.behavior.topology.zoom), + camera + ); + assert_eq!(app.document.bundle(), &source); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_process_retained_toolbar_intents_reject_chooser_dirty_and_picker_lifetimes() { + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1440., 900.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + retained_toolbar_boundaries(&mut mounted, &mut app, "OR").await; + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} + +#[tokio::test] +async fn production_process_keyboard_reveals_and_activates_initially_offscreen_last_tool() { + use crate::behavior::canvas_workspace::tests::*; + let f = super::super::direct_tests::fixture(Layer::Oa); + let mut app = App::with_project(f.p); + let _ = app.update(Message::Select(f.owner)); + let _ = app.update(Message::Page(crate::app::Page::Behavior)); + app.window_size = Size::new(1100., 420.); + let mut mounted = Mounted::new(&mut app).await; + let snapshot = app.document.snapshot(); + let source = app.document.bundle().clone(); + overflow_last_tool(&mut mounted, &mut app, "->").await; + assert_eq!(app.behavior.topology.tool, CanvasTool::Connect); + assert!(std::sync::Arc::ptr_eq(&snapshot, &app.document.snapshot())); + assert_eq!(app.document.bundle(), &source); + assert!(!app.document.can_undo() && !app.document.can_redo()); +} diff --git a/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/view.rs b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/view.rs new file mode 100644 index 00000000..4302d849 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-06ec962/raw-inputs/crates/oa-desktop/src/behavior/topology/view.rs @@ -0,0 +1,595 @@ +use super::*; +use iced::widget::column; +pub(super) fn picker<'a>( + mut choices: Vec, + d: &'a Draft, + index: usize, + value: Option, + placeholder: &'a str, + change: fn(Choice) -> Event, +) -> Element<'a, Message> { + let selected = value + .and_then(|id| choices.iter().find(|c| c.id == id)) + .cloned(); + let browser = &d.browsers[index]; + let query = browser.search.trim().to_lowercase(); + choices.retain(|c| c.label.to_lowercase().contains(&query)); + let range = choice_page(choices.len(), browser.page); + let page = range.start / 24; + let mut out = column![ + text_input("Search compatible choices…", &browser.search) + .on_input(move |v| event(Event::Search(index, v))) + .padding(7) + .size(12) + ] + .spacing(5); + if let Some(selected) = selected + .as_ref() + .filter(|c| !choices[range.clone()].iter().any(|v| v.id == c.id)) + { + out = out.push(label(format!("Selected: {}", selected.label))); + } + out = out.push( + row![ + button("Previous") + .on_press_maybe((page > 0).then(|| event(Event::Page(index, page - 1)))), + text(format!( + "{} choices · {}/{}", + choices.len(), + page + 1, + choices.len().div_ceil(24).max(1) + )) + .size(11), + button("Next").on_press_maybe( + (range.end < choices.len()).then(|| event(Event::Page(index, page + 1))) + ) + ] + .spacing(6), + ); + out.push( + pick_list(choices[range].to_vec(), selected, move |v| event(change(v))) + .placeholder(placeholder) + .padding(8) + .text_size(12) + .width(Fill), + ) + .into() +} +pub(super) fn choice_page(len: usize, requested: usize) -> std::ops::Range { + let start = requested.min(len.saturating_sub(1) / 24) * 24; + start..(start + 24).min(len) +} +pub(super) fn view<'a>(app: &'a App, owner: Id, out: Column<'a, Message>) -> Element<'a, Message> { + let p = app.project(); + let context = app.topology_context().expect("visible process context"); + let state = &app.behavior.topology; + let pending = state.draft.is_some(); + let ordered = !legacy_order(p, owner).is_empty(); + let selected = is_contribution(p, owner, app.selected).then_some(app.selected); + let clean = !app.behavior.has_pending() && !app.draft_dirty() && !app.source_dirty(); + // A fixed sibling keeps Iced's canvas scroll state through selection/form changes. + let mut navigation = column![].spacing(6); + if let Some(Relation { + body: + RelationBody::ContextualLink { + owner: caller, + source, + target, + purpose, + }, + .. + }) = p.relations.get(&app.selected) + && *caller == owner + { + navigation=navigation.push(text(format!("From: {}",super::contextual::address_label(p,owner,source))).size(12)) + .push(text(format!("To: {}",super::contextual::address_label(p,owner,target))).size(12)) + .push(text(match purpose {ContextualLinkPurpose::Precedence {..}=>"Precedence · hidden contributions attach visually to their folded reference",_=>"Assembly incidence · no execution order is implied"}).size(11)); + } + if let Some(address) = state.qualified_selection(p, owner) { + navigation=navigation.push(text(super::contextual::address_label(p,owner,&address.endpoint)).size(12)) + .push(text("Qualified contribution · definition properties affect every use of the source.").size(11)) + .push(button("Open contribution definition").on_press(Message::TopologyCanvas(CanvasEvent::OpenQualified(context,address.endpoint.clone())))); + if matches!( + p.elements + .get(&address.endpoint.occurrence) + .and_then(|e| e.shared()), + Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { .. }, + .. + }) + ) { + navigation = navigation.push( + button(if state.contextual_expanded.contains(&address.endpoint) { + "Fold nested reference" + } else { + "Expand nested reference" + }) + .on_press(Message::TopologyCanvas(CanvasEvent::ExpandQualified( + context, + address.endpoint.clone(), + ))), + ); + } + } + if let Some(id) = selected { + let mut actions = + row![button("Contribution properties").on_press(Message::EditProperties(id))] + .spacing(6); + if let Some(target) = direct::referenced(p, owner, id) { + actions = actions.push( + button(text(format!("Open source: {}", short(p.named(target), 28))).size(12)) + .on_press(Message::TopologyCanvas(CanvasEvent::Open(context, id))), + ); + if matches!( + p.elements[&id].shared(), + Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { .. }, + .. + }) + ) { + actions = actions.push( + button(if state.expanded.contains(&id) { + "Collapse" + } else { + "Expand" + }) + .on_press(Message::TopologyCanvas(CanvasEvent::Expand(context, id))), + ); + } + } + navigation = navigation.push(actions); + } else if sequence_links(p, owner) + .iter() + .any(|r| r.id == app.selected) + { + navigation = navigation.push( + button("Path properties / guard").on_press(Message::EditProperties(app.selected)), + ); + } + if !state.return_to.is_empty() { + navigation = navigation.push( + button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))), + ); + } + navigation = navigation + .push( + button("Two paths...").on_press_maybe(clean.then_some(event(Event::Start( + owner, + Tool::Branch, + selected, + )))), + ) + .push( + button("Qualified link / assembly...").on_press_maybe(clean.then_some(event( + Event::Start(owner, Tool::Contextual, Some(app.selected)), + ))), + ); + if ordered { + navigation = navigation.push(button("Append exchange...").on_press_maybe(clean.then_some( + Message::TopologyCanvas(CanvasEvent::AppendExchange(context)), + ))); + let commands = preserved_order(p, owner, None); + if !commands.is_empty() { + navigation = navigation.push( + button("Edit path links and guards") + .on_press_maybe(clean.then_some(Message::Command(unique_batch(p, commands)))), + ); + } + } + let mut graph = Graph::expanded_qualified( + p, + owner, + app.selected, + &state.expanded, + &state.contextual_expanded, + ); + graph.scale = state.scale(graph.size, (app.window_size.width - 600.).max(200.)); + graph.camera_pan = state.pan; + graph.pending_insertion = state + .draft + .as_ref() + .filter(|d| d.owner == owner && d.fixed) + .map(|d| d.insertion); + graph.interaction = Some((context, state.tool, clean)); + let scale = graph.scale; + let scope = super::super::canvas_workspace::Scope { + document: context.document, + owner, + revision: context.revision, + epoch: app.camera_epoch, + }; + let keyboard = clean && super::super::canvas_workspace::keyboard_enabled(app); + let camera = move |e| Message::TopologyCanvas(CanvasEvent::Camera(context, scope.epoch, e)); + let canvas = container( + iced::widget::sensor( + super::super::canvas_workspace::Viewport::new( + canvas::Canvas::new(graph).width(Fill).height(Fill), + scope, + scale, + keyboard, + camera, + ) + .pan_background(state.tool == CanvasTool::Select), + ) + .key((context.document, owner)) + .on_show(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size))) + .on_resize(move |size| Message::TopologyCanvas(CanvasEvent::Viewport(context, size))), + ) + .id("process-viewport") + .width(Fill) + .height(Fill) + .style(style::card) + .into(); + let header = column![ + row![ + out.width(Fill), + super::super::canvas_workspace::Control::new( + button( + text(if state.focused { + "Restore panels" + } else { + "Focus canvas" + }) + .size(12) + ) + .padding(6) + .on_press(super::super::canvas_workspace::intent( + scope, + "Panels", + Message::TopologyCanvas(CanvasEvent::Focus(context)) + )), + scope, + "Panels", + keyboard, + Some(super::super::canvas_workspace::intent( + scope, + "Panels", + Message::TopologyCanvas(CanvasEvent::Focus(context)) + )), + camera + ) + ] + .spacing(6), + super::super::canvas_workspace::controls( + scope, + keyboard, + scale, + event(Event::Shelf(Shelf::Selection)), + clean.then_some(Message::TopologyCanvas(CanvasEvent::Camera( + context, + scope.epoch, + super::super::canvas_workspace::Event::Focus + ))), + move |zoom| Message::TopologyCanvas(CanvasEvent::Zoom(context, zoom)), + camera + ) + ] + .spacing(4); + let entries = [ + ("S", "Select contributions or paths", CanvasTool::Select), + ( + "W", + "Work: click a path or empty space", + CanvasTool::Add(Tool::Work, ControlKind::Choice), + ), + ( + "E", + "Exchange: click a path or empty space", + CanvasTool::Add(Tool::Exchange, ControlKind::Choice), + ), + ( + "ref", + "Reuse process / chain", + CanvasTool::Add(Tool::Reuse, ControlKind::Choice), + ), + ( + "OR", + "Choice", + CanvasTool::Add(Tool::Control, ControlKind::Choice), + ), + ( + "M", + "Merge", + CanvasTool::Add(Tool::Control, ControlKind::Merge), + ), + ( + "AND", + "Fork", + CanvasTool::Add(Tool::Control, ControlKind::Fork), + ), + ( + "J", + "Join", + CanvasTool::Add(Tool::Control, ControlKind::Join), + ), + ( + "IT", + "Loop", + CanvasTool::Add(Tool::Control, ControlKind::Loop), + ), + ( + "->", + "Sequence link: click or drag between contributions", + CanvasTool::Connect, + ), + ]; + let palette = super::super::canvas_workspace::palette( + scope, + keyboard, + camera, + entries.into_iter().map(|(glyph, label, choice)| { + ( + glyph, + label, + choice == state.tool, + clean.then_some(Message::TopologyCanvas(CanvasEvent::Choose( + context, choice, + ))), + ) + }), + ); + let details = scrollable(navigation.push(text("Layout is derived. The reorder handle changes display order within its column; it does not author a position.").size(11))) + .id("process-details").width(Fill).height(Fill).into(); + let mut form_panel: Element<'_, Message> = iced::widget::Space::new().height(0).into(); + if let Some(d) = state.draft.as_ref().filter(|d| d.owner == owner) { + if let Some(child) = d.source.view(p) { + form_panel = column![ + text(format!( + "Consumer name: {}", + crate::views::short(&d.name, 60) + )) + .size(12), + child.map(|e| event(Event::Source(e))) + ] + .spacing(6) + .height(Fill) + .into(); + } else { + let mut form = column![text(d.tool.title()).size(16)].spacing(8); + if let Some(contextual) = &d.contextual { + form = form + .push( + text_input("Link name (retain current when blank)", &d.name) + .on_input(|v| event(Event::Name(v))) + .padding(8) + .size(12), + ) + .push(super::contextual::view(p, owner, contextual)); + } else { + if let Some(link) = d.insertion.link.and_then(|id| p.relations.get(&id)) { + let guard = if let RelationBody::SequenceLink { guard, .. } = &link.body { + guard.as_str() + } else { + "" + }; + form = form.push(label(format!( + "Insert on {} → {} · preserve path identity and condition [{}]", + p.named(link.source()), + p.named(link.target()), + guard + ))); + } else if d.fixed && ordered { + form = form.push(label(match (d.before, d.after) { + (Some(before), Some(after)) => format!( + "Insert between {} → {} · preserve the ordered path", + p.named(before), + p.named(after) + ), + (Some(before), None) => format!("Continue after {}", p.named(before)), + (None, Some(after)) => format!("Insert before {}", p.named(after)), + _ => "Start the ordered path".into(), + })); + } else if d.fixed + && d.tool == Tool::Exchange + && let Some(before) = d.before + { + form = form.push(label(format!( + "Append after {} → new exchange · Create adds an explicit sequence link", + p.named(before) + ))); + } else if d.fixed && d.tool != Tool::Connect { + form = form.push(label( + d.insertion + .before + .map_or("Append an unconnected contribution".into(), |id| { + format!("Unconnected contribution before {}", p.named(id)) + }), + )); + } + if d.tool == Tool::Work { + if let Some(title) = d.source.label(d.target) { + form = form.push(label(title)).push(label("Create commits the new work and contribution together at the chosen path position.")); + } + form = form.push(button("Create new source…").on_press(event(Event::Source( + super::super::source_draft::Event::Open(d.invocation), + )))); + } + if !legacy_order(p, owner).is_empty() { + form = form.push(label( + "This edit preserves the ordered path as explicit links in the same undo step.", + )); + } + if matches!( + d.tool, + Tool::Work | Tool::Exchange | Tool::Reuse | Tool::Branch + ) { + let choices = targets(p, owner, d.tool) + .into_iter() + .filter(|c| compatible_source(p, d, c.id)) + .collect(); + if d.tool == Tool::Branch { + form = form.push(label("First path work")); + } + form = form.push(picker( + choices, + d, + 0, + d.target, + if d.tool == Tool::Branch { + "Choose first path work…" + } else { + "Choose existing source…" + }, + Event::Target, + )); + } + if d.tool != Tool::Connect { + form = form.push( + text_input("Contribution name (defaults to source)", &d.name) + .on_input(|v| event(Event::Name(v))) + .padding(8) + .size(12), + ); + } + if matches!(d.tool, Tool::Control | Tool::Branch) { + let kinds = if d.tool == Tool::Branch { + vec![ControlKind::Choice, ControlKind::Fork] + } else { + vec![ + ControlKind::Choice, + ControlKind::Merge, + ControlKind::Fork, + ControlKind::Join, + ControlKind::Loop, + ] + }; + form = form.push( + pick_list( + kinds.into_iter().map(ControlChoice).collect::>(), + Some(ControlChoice(d.control)), + |v| event(Event::Control(v)), + ) + .padding(8) + .text_size(12), + ); + } + if d.tool == Tool::Branch { + form = form.push(label("Second path work")).push(picker( + targets(p, owner, Tool::Work), + d, + 1, + d.alternate, + "Second path work…", + Event::Alternate, + )); + if d.control != ControlKind::Fork { + form = form + .push(label("First path guard")) + .push( + text_input("First path guard", &d.guard) + .on_input(|v| event(Event::Guard(v))) + .padding(8), + ) + .push(label("Second path guard")) + .push( + text_input("Second path guard (e.g. else)", &d.alternate_guard) + .on_input(|v| event(Event::AlternateGuard(v))) + .padding(8), + ); + } + } + if d.fixed { + if let (Some(a), Some(b)) = (d.before, d.after) { + form = form.push(label(format!("{} → {}", p.named(a), p.named(b)))); + } + } else { + form = form + .push(label("Preceding contribution")) + .push(picker( + contributions(p, owner), + d, + 2, + d.before, + "After contribution…", + Event::Before, + )) + .push(button("Clear preceding").on_press(event(Event::ClearBefore))) + .push(label("Following contribution")) + .push(picker( + contributions(p, owner), + d, + 3, + d.after, + "Before contribution…", + Event::After, + )) + .push(button("Clear following").on_press(event(Event::ClearAfter))); + } + if d.tool != Tool::Branch + && d.insertion.link.is_none() + && (d.before.is_some() || d.tool == Tool::Connect) + { + form = form.push( + text_input( + "Path condition (empty remains a Choice / Loop warning)", + &d.guard, + ) + .on_input(|v| event(Event::Guard(v))) + .padding(8) + .size(12), + ); + } + } + let valid = match d.command(p) { + Ok(_) => true, + Err(problem) => { + form = form.push(label(problem)); + false + } + }; + let actions = row![ + button(if d.contextual.is_some() { + "Apply link" + } else { + "Create" + }) + .on_press_maybe(valid.then(|| event(Event::Submit))), + button("Cancel").on_press(event(Event::Cancel)) + ] + .spacing(7); + form_panel = editor_panel(d.invocation, actions.into(), form.into()); + } + } + let editor = if pending { + Some(form_panel) + } else { + state.shelf.map(|_| details) + }; + super::super::canvas_workspace::host(header.into(), palette, canvas, editor) +} + +fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>( + session: Id, + actions: iced::Element<'a, Message, iced::Theme, R>, + fields: iced::Element<'a, Message, iced::Theme, R>, +) -> iced::Element<'a, Message, iced::Theme, R> { + container( + container( + column![ + container(actions).id("process-actions"), + super::super::form_session::reset_on_change( + session, + scrollable(fields).id("process-form").height(Fill) + ) + ] + .spacing(8), + ) + .width(Fill) + .height(Fill) + .padding(12) + .style(style::card), + ) + .width(Fill) + .height(Fill) + .padding(iced::Padding { + top: 0., + right: 18., + bottom: 18., + left: 18., + }) + .into() +} + +#[cfg(all(test, debug_assertions))] +#[path = "layout_tests.rs"] +mod layout_tests; diff --git a/harness/experiments/canvas-union-windows-20260911/tested-ff01754/artifact-manifest.json b/harness/experiments/canvas-union-windows-20260911/tested-ff01754/artifact-manifest.json new file mode 100644 index 00000000..d6018693 --- /dev/null +++ b/harness/experiments/canvas-union-windows-20260911/tested-ff01754/artifact-manifest.json @@ -0,0 +1,27 @@ +[ + { + "name": "arcadia-studio-tests.exe", + "bytes": 139577856, + "sha256": "63DE2FF7FB4C5E49C1E59378EAE24D77471AF35FADADFAEC5871956E7B2E0D8D" + }, + { + "name": "full-ff01754.log", + "bytes": 83584, + "sha256": "A6D2E0D39B45246D88F1F5B61117CDB0897B23A0D072238ECA7A5550495BD421" + }, + { + "name": "full-ff01754.result.json", + "bytes": 254, + "sha256": "C448190DE3C3768E43C0E191274D81074927A74C91C6EE7E4DC9AD5369C2B92D" + }, + { + "name": "full-inputs-ff01754.json", + "bytes": 96552, + "sha256": "966B125EC8862D0E48ED98692BC3DC380F0242007EB4C018621A9FDE844562EB" + }, + { + "name": "libunwind.dll", + "bytes": 190464, + "sha256": "AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A" + } +] diff --git a/harness/release-activity-allocation-drag-contract-v2-20260911.md b/harness/release-activity-allocation-drag-contract-v2-20260911.md new file mode 100644 index 00000000..f1f83e19 --- /dev/null +++ b/harness/release-activity-allocation-drag-contract-v2-20260911.md @@ -0,0 +1,122 @@ +# Allocation drag contract v2 — 2026-09-11 + +Codex-956atf `/root/capella_process_gap`. Implementation-ready proposal for root ratification of [issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3). The [original design](release-activity-allocation-drag-design-20260911.md) and [independent review](release-activity-allocation-drag-review-20260911.md) remain immutable. Root accepted all five review corrections; this document resolves them without claiming implementation, issue closure or full-goal completion. + +Source inspected at **ff0175488ac57a92d401b00a0e4542582a82d8e9** on `codex/behavior-authoring-20260910`. [Peer issue 4 update 446](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/4#issuecomment-446) now transfers both Allocation and RoleAssignment identities to the first child; that remains peer-owned. This task creates only this report. No production/test/Git/index/compiler/native/Linux/WSL/VPN changes or agents. + +## Decisions from the review + +| Finding | Binding v2 resolution | Revisit criterion | +| --- | --- | --- | +| 1. Same-performer role-only drag could create Allocation | Ordinary drop onto the unique current effective performer is pure layout, for leaves and uniformly assigned composites, regardless of direct/role provenance. Explicit Assign is a separate operation. Unsupported role exits refuse without moving. | Add a deliberate role/assignment editing workflow with an exact preview; never reinterpret ordinary arrangement. | +| 2. Deleted-ID checks miss surviving-record changes | Compare the entire prepared candidate against an expected Project containing only intended allocation edits and permitted presentation changes. Constraint cleanup, trace/artifact edits and cascading mappings fail this gate. | A specific dependency-repair workflow defines and displays its additional edits. | +| 3. Press selection can auto-apply dirty properties | New clean-specific work selection and gesture admission; no `SelectNode`, `select()`, `apply()` or legacy MoveNode fallthrough. A dirty ordinary click on eligible work preserves current selection/forms and explains refusal. | Any broader selection redesign must explicitly preserve draft identity and may not silently apply it. | +| 4. Mixed composite geometry is not solved by translation/reflow | Distinct semantic work closure and active-diagram placement closure; first slice translates a coherent uniform subtree as one block. Distributed/mixed convergence and unreadable candidates refuse. No invented placements or automatic sibling packing. | A bounded placement algorithm preserves identity, shows its exact changes and passes the mixed P/x/y counterexample. | +| 5. Target rectangles and semantic preview can disagree | Frozen, visibly outlined target hit regions with semantic ancestor precedence and explicit occlusion. Candidate Project uses its own ViewIndex; cache includes destination/semantic plan identity. | Relax only with evidence that displayed targets, hit resolution, candidate grouping and final state remain identical. | + +The primary choice remains ordinary context-sensitive work dragging in Allocation views. A dedicated assignment tool is easier to isolate but does not fix the user's natural gesture. Adopt ordinary dragging with a transient Arrange only alternative. Likewise, immediate mixed-subtree packing would broaden this slice; a specific visible refusal is preferable until a real placement algorithm exists. These are staged implementation boundaries, not reduced OA/SA requirements. + +## Admission, selection and lifetime + +Eligible pointer bodies are Activity/Function placements in `ProjectionKind::Allocation`. Ports, edges and controls retain their own gesture priority. Every eligible work-body press enters this contract, including stationary clicks and Arrange only; refusal must never fall back to legacy movement. + +A **clean owner** means the current Diagram page and active allocation diagram, accepted document, no dirty property/source/behavior draft, no architecture creation/connection session, and no modal/menu/chooser/file/save/hosted operation owning input. App supplies a render-time capability but rechecks it when receiving press, preview and release events. + +- **Clean stationary press:** select the represented work and exact placement through `select_work_clean`. It checks the capability, sets selection, and may rebuild a clean property display. It does not call any applying selection helper, change source/history/revision, run Fit or invalidate this drag's lifetime. +- **Dirty stationary press or attempted drag:** preserve selection, draft contents, cursor/focus and source exactly. Do not arm movement; show “Finish or cancel the current edit before selecting or moving work.” Clicking the already selected work also preserves its dirty form. Within the Allocation canvas, intercept dirty left-button object presses before legacy node/port selection, so a nearby port hit cannot auto-apply the same draft. This is a local pointer-selection policy; explorer/text navigation is outside this change. An input-owning overlay keeps its own focus. +- **Clean drag:** capture a fresh gesture token, document instance/revision, diagram/projection, input epoch, exact canvas bounds/pan/zoom, work/placement IDs, original scope/provenance and frozen hit geometry. App registers the token on the clean press event. Selection alone must not retire it. +- Owner/document/revision/projection, source/property/behavior dirtiness, modal/busy ownership, tool, bounds, camera, window focus loss or Escape retires the token. Even a Fit/tool action that returns to the same numeric values retires it. No stale packet becomes valid again when a modal closes or Undo restores prior values. + +Retain ordered native pointer positions and the existing 3-screen-pixel threshold. A fast press→motion→release batch must work without waiting for a Task/round-trip acknowledgement: App processes the stamped press before its stamped release and verifies their common token. Outside/hidden release cancels. There is at most one final command per token. + +**Arrange only:** a visible “Arrange next drag” control arms one gesture and displays an armed indicator. Keyboard activation is supported; Escape, cancellation, completion or owner/lifetime change clears it. Capture the mode at press and consume it then; it cannot remain sticky after a gesture or switch interpretation midway through one. It changes presentation only. + +## Scope and semantic destination + +For selected work `w`, let **S** contain w and all recursive same-kind semantic work children; **L** is the nonempty subset with no such child. Traverse accepted semantic parents, not rendered groups, and include folded/unplaced leaves. Detect stale/missing/cyclic scope before planning. Let **Q** be every existing placement in the active diagram whose element is in S, including hidden placements. The selected placement belongs to Q. Do not create placements, move other diagrams' nodes, or include unrelated work merely because it is geometrically enclosed. + +Classify each leaf by exact direct Allocation identity (zero/one), RoleAssignment/RoleRealization references, and current `effective_performer`. The supported uniform cases are: + +- **Assigned(P):** every leaf has the same unique effective performer P, including role-only and mixed direct/role provenance. +- **Unassigned:** every leaf has neither direct Allocation nor RoleAssignment. +- Otherwise **MixedOrUnresolved**. This is a valid modeling state where admitted by core. Composite group dragging, including Arrange only, is a named `MixedLayoutRequired` refusal in this slice; select a specific leaf instead. A leaf with unresolved role responsibility may still be arranged or explicitly dropped onto B: B is chosen by the user, not inferred from its roles. Its outward role exit remains refused below. + +Ordinary destination means `Performer(P)` or `OutsidePerformers`; Arrange only means `LayoutOnly` independent of the pointed performer. + +| Operation | Intended semantic change | +| --- | --- | +| LayoutOnly, or Assigned(P) dropped onto P | None. In particular, create no direct Allocation for a role-only leaf. | +| Unassigned, Assigned(A) dropped onto a different B, or a single leaf without a unique performer dropped onto B | For each leaf: edit an existing Allocation's source only, preserving complete header/identity/target; otherwise create one B → leaf relation with an identity allocated once per gesture/leaf. No parent Allocation. Preserve all role relationships; existing advisory role warnings stay advisory. If roles were unresolved, label the explicit assignment and retained role responsibility. | +| Assigned(A) dropped outside; every leaf has one direct allocation and removing them leaves no role responsibility | Delete exactly those Allocation IDs. Keep work decomposition and work/port identities. Label “Remove assignment — decomposition retained.” | +| Outside drop with role-only responsibility, or removal revealing retained/ambiguous role responsibility | `RoleExitRequired`: unchanged model **and placement**. Name the retained role/performer. If direct A would reveal B, say “Removing direct assignment would place work in B through its role”; do not call it Unallocated. | +| Unassigned dropped outside | Layout only; no relation added/deleted. | + +Explicitly assigning direct Allocation to an already effective performer remains an existing editor/API action, not ordinary dragging. For a semantic child, “outside performer” does not promise source-root or diagram-root placement: its work parent remains intact, and resulting ancestor groups must be shown. + +## Stable target resolution + +Freeze **visible accepted** performer rectangles, header/body bounds, semantic performer ancestry, card occluders and viewport clip at press. During preview render the active frozen target outline above the ghost, distinctly from the candidate's enclosure; retain all candidate-group feedback. Hidden performers are not targets. A collapsed performer is targetable only by its visible header; successful semantic assignment may unfold that target placement in the same Batch. + +Use the ordered release pointer in the frozen transform: + +1. Outside the captured viewport or on an overlay/control/port/edge handle is invalid; it is never an outgoing drop. +2. Exclude both Q's accepted rectangles and its moving ghost from occluders. A different visible work or other non-performer card blocks target selection in its own painted body, including an underlying performer; it never implies reparenting. This rule requires aiming at an exposed performer header/body. Blank areas between its child cards remain targetable. +3. Collect compatible performer regions containing the pointer. Use half-open rectangles (left/top included, right/bottom excluded) for exact boundary behavior. If all candidates form one chain in **semantic parent ancestry**, choose its deepest candidate. Geometric rectangle containment does not establish this relationship. +4. Two incomparable candidates mean `AmbiguousTarget`, even when one rectangle geometrically contains the other. Highlight both and ask the user to use an exposed region; do not choose by paint order, size or UUID. +5. No performer hit and no blocking card means OutsidePerformers. Current-performer body remains an ordinary same-effective layout destination. + +Camera/bounds change invalidates rather than rebasing frozen targets. Candidate enclosure changes never change this resolver. Active target, status wording and committed destination must match, including when the pointer coordinates remain constant. + +## Placement and readable candidate rule + +First-slice geometry is **one uniform translation**, not packing: + +- Compute the requested delta from the selected placement's original position, press offset and current ordered pointer. Snap that root position to the existing 10-world-unit grid, then apply the identical delta to every Q position, including hidden children. Retain each node's size, anchors and fold state except the explicitly shown target unfold. +- Before permitting a composite semantic move, require its currently placed work closure to form one projected subtree under the selected root when that root's folded subtree is expanded in a read-only geometry projection. A distributed mixed closure is not made coherent merely by applying a common delta. +- Use `translated_routes` with that **captured Q delta map**, not a new descendant walk after changing allocation. This translates an active-diagram route only under the existing both-endpoint/same-delta rule; one-ended pins and label offsets retain their current contract. Do not move target contents or unrelated siblings. +- Project the complete candidate, including ancestors whose allocation summaries change. All coordinates/dimensions must pass current core bounds. The moved expanded subtree's internal relative positions and footprint remain unchanged by translation; unplaced leaves do not add ghost cards. +- Reject `LayoutRequired` if the result introduces or enlarges positive-area overlap between unrelated projected card interiors (compare the same pair before/after; ancestor/descendant enclosure is exempt). Check the read-only expanded moved subtree as well as the visible scene, so folding cannot hide a newly introduced collision. No automatic `insertion_adjustments` packing is promised here. +- The selected work header and, when targeting a performer, its active frozen target label must be visible in the viewport at the actual drag zoom. Outside/layout-only status remains visible beside the ghost or in the fixed canvas status. If the candidate hides the required header/label, refuse with “Make room or adjust the view, then drag again.” Keep the camera fixed; do not silently Fit an unreadable result. Larger preexisting diagrams are allowed; this does not require the whole diagram to fit. + +These are deterministic first-slice gates, not evidence that all large-model arrangements are usable. Native review must verify that they allow the common empty/incoming/between/outgoing workflows. If normal target contents make refusal frequent, implement a bounded **visible** block-placement algorithm and revisit finding 4 before broadening acceptance. + +## Exact candidate/diff gate + +Build an **expected Project** from the accepted one by applying only the selected allocation additions/source edits/removals directly to a scratch copy and the enumerated permitted presentation edits. Separately obtain the actual candidate by `prepare_command`/`execute_batch` using ordinary commands, including their deferred cleanup and full core validation. Require equality of all Project fields/maps with the expected Project. + +Permitted presentation changes are only Q node positions; the active target's displayed unfold; exact active-diagram route changes from the captured delta map; and removal of routes whose `edge` is an intentionally deleted Allocation ID. **Own-route cleanup is allowed in any diagram** because the route cannot survive its deleted relation; all other inactive-diagram geometry remains identical. Do not whitelist general view-option cleanup or other saved-view changes. + +Consequently all element bodies/parents, non-target relation records, project headers/schema, artifacts, traces/baselines and untouched diagram fields must be identical. Check surviving records as well as missing IDs. A Constraint losing its `constrained` reference, dependent PortAllocation/ExchangeAllocation deletion, or trace/artifact mutation yields `CollateralChangeRequired` with named changed records and no accepted movement. This is a scoped equality gate, not a new generic impact framework. + +Core errors produce `InvalidCandidate`; permitted warnings remain visible and do not become new errors. Source compilation must succeed before commit. Recheck the exact gate at release; no preview grants mutation authority. Execute the same stable-ID Batch through `Document::execute` once. Source refusal preserves accepted Project/SourceBundle/history and displays a retryable reason; retry recomputes against the live revision. A snapped no-op emits no semantic edits and no history entry. + +## Concrete interfaces and preview/cache ownership + +New names are proposed seams, not existing APIs: + +| Boundary | Contract | +| --- | --- | +| `capture(project, diagram, work, placement)` | Pure Scope: S/L/Q IDs, exact original records/provenance, uniform class; no model mutation or GUI selection. | +| `plan_semantics(project, scope, destination, allocation_ids)` | Pure `SemanticPlan` or typed Refusal. Stable intended edits, label, complete semantic candidate Project and candidate `ViewIndex::new(candidate)`. IDs are allocated once outside the pure function and reused when revisiting a target. | +| `place(plan, snapshot, delta)` | Candidate Diagram + moved-route edits, projection, readability result and final Batch. It reuses the semantic candidate/index; it does not revalidate/clone all semantics per pointer pixel. | +| `WorkDragEvent::PressClean(stamp, token, node)` | App rechecks clean owner, registers token/scope and performs clean-specific selection. Never translates to legacy SelectNode. | +| `WorkDragEvent::Preview(stamp, token, destination, point)` | App checks token, resolves/cross-checks destination against frozen geometry, and returns/displays the keyed plan/placement or refusal. Ignore old responses. | +| `WorkDragEvent::Release(stamp, token, destination, point)` | Recheck clean owner, token, captured facts and current target; recompute final geometry/diff; execute one Batch or visible refusal. Consumes token. No fallback MoveNode/apply. | +| `Cancel(token)`, `Refused(stamp, reason)`, `ArrangeNext` | Presentation only. Refusal never changes selection/forms; cancel retires token and one-shot mode. | + +Keep gesture-local ordered input/ghost state in `diagram.rs`; keep command admission and clean selection in a narrow root-owned App handler; use a small `allocation_drag.rs` planner. No edits to peer CreateContextElement logic, allocation validation, source grammar/schema or a new generic draft framework. + +Cache semantic work by **(document/revision, gesture token, captured scope/provenance identity, mode, semantic destination)**. A destination change rebuilds candidate semantics/index even if snapped position is unchanged. Within one plan, geometric cache keys additionally include snapped delta, target unfold and preview geometry identity. Draw, ports, route eligibility, routing, hit-related candidate feedback and export-style projection must all use the same candidate Project/index. The accepted semantic index may be reused only for a plan with identical semantics. All caches are transient and invalidated with the token/lifetime. + +## Required nontrivial verification + +No tests were run or added by this task. Before claiming this slice: + +- **Role counterexamples:** same-effective role-only leaf and uniform composite arrangement leave Allocation empty. Direct/role mixture under A stays byte-identical semantically on ordinary drop in A. Different B creates/retargets exact intended leaf records and retains advisory warnings. Role-only outward and A→role-B fallback produce truthful refusal with unchanged placement. +- **Hidden semantic edits:** delete Allocation referenced by a surviving Constraint; candidate remains core-valid but must fail exact diff. Repeat artifacts/traces and SA mapping cascades. Conversely a deleted Allocation's own stored routes can disappear across diagrams while every other saved-view field stays exact. OA communication incompleteness that is warning-only remains warning-only. +- **Mounted admission:** dirty property form before actual press→selection→move→release; dirty source/behavior and modal/save ownership; no apply/history/selection change. Clean stationary click selects correctly without revision change; full same-batch clean drag commits once. Replay captured press/preview/release after return from modal, Fit/resize or revision change and verify rejection. +- **Scope/layout:** uniform first/second/grandchild trees, folded children, unplaced leaf, other diagram with different placement; move exactly Q while editing every intended L allocation. Peer first-child transfer of Allocation **and RoleAssignment** invalidates old scope. Mixed P with x under A and y under B refuses rather than stretching C. Check active ancestor regrouping and introduced overlaps. +- **Targets/cache:** nested semantic performers versus overlapping/included peers, exact shared borders, work-card occlusion, collapsed headers, outgoing blank region and outside viewport. Same snapped point with destination A→B rebuilds semantic preview/index; rendered target and committed target agree. Repeated motion within B reuses its semantic plan. +- **End-to-end OA/SA:** ordinary leaf in→between→out and uniform composite move at 1100×700 and wider; full Project/SourceBundle Save/reopen and exact one-step Undo/Redo, including relation metadata, hidden placements, routes and source comments. Escape, unfocus, snapped no-op and source refusal leave no partial result. Verify actual packaged Windows target visibility/readability; compilation or planner passes are insufficient. + +Mixed-layout convergence, deliberate role exit and collateral dependency repair remain required named continuations with explicit current refusals. The full OA/SA and cross-platform goals remain unchanged; Linux/WSL remains user-paused. diff --git a/harness/release-activity-allocation-drag-design-20260911.md b/harness/release-activity-allocation-drag-design-20260911.md new file mode 100644 index 00000000..4b9d5723 --- /dev/null +++ b/harness/release-activity-allocation-drag-design-20260911.md @@ -0,0 +1,104 @@ +# Activity/function allocation drag proposal — 2026-09-11 + +Codex-956atf `/root/capella_process_gap`, advisory diagnosis/design for [issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3), under [root scope 432](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3#issuecomment-432). + +The user wants existing activities to move from the canvas root into entities/actors and back out by dragging. The proposal below also covers SA functions/system actors. It changes allocation and placement, never work decomposition. This is a proposal for root review, not ratified specification, implementation or native acceptance. + +Read current AGENTS/PROJECT_GOAL, issue 3/4 and current diagram, App, projection, validation, command and reference-deletion source. Baseline anchors identify **638d06c4e33a1f54203b234d0b49c3ecf551d27d**. Root's canvas integration currently has local staged/unstaged changes and merge conflicts; none was modified. [Issue 4 comment 431](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/4#issuecomment-431) assigns the child-work correction to ytr83r: first-child creation transfers the existing leaf allocation to the child, preserving identity/metadata. This report does not acquire that implementation, B4/header or source-navigation work. Only this report was created; no compiler/test/native execution, Git/index/Wiki mutation, agents, Linux/WSL or VPN activity. + +## Exact cause of the current interaction + +1. [Diagram drag state][S1] stores placement ID, pointer origin and original position. Press starts a placement drag; release emits only `Message::MoveNode(id, position)`. There is no performer destination or allocation intent. +2. [App's MoveNode handler][S2] applies pending properties and executes `Command::MoveNode { with_descendants: true }`. [Core movement][S3] updates node positions and eligible route waypoints. It never adds, edits or removes Allocation. +3. In an allocation diagram, [projection parent computation][S4] groups work under its computed performer, or under a visible work ancestor with the same performer. Parentage comes from semantic records, not geometric overlap. +4. [Enclosure][S5] expands/moves a projected parent to surround its projected children. The [drag preview][S6] retains the accepted Project's allocation relationships, so dragging work outward keeps it inside the same expanding enclosure. + +Thus incoming drag cannot create allocation; outgoing drag cannot remove it. The continued enclosure is expected projection behavior, not a coordinate-clamping defect. Fixing coordinates, weakening enclosure, or enabling arbitrary `Reparent` would address the wrong layer. This diagnosis follows executed source paths; it is not a fresh observation of the native binary. + +## Required meaning and less obvious cases + +The existing [language contract][S7] distinguishes Activity → Activity / Function → Function decomposition from participant → leaf-work Allocation. Non-leaf Allocation is invalid; each leaf has at most one direct performer. Actor graphical containment must therefore not become `Element.parent = actor`. [The earlier native Capella benchmark](release-capella-basic-editing-20260907.md) independently found an activity visually inside an entity while the activity remained in its work hierarchy with a separate allocation. Capella also separates [allocation management](https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/doc/plugins/org.polarsys.capella.ui.doc/html/07.%20Modelling%20Wizards%20and%20Accelerators/7.2.%20Contextual%20Allocations.mediawiki) from [layout-only work](https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/doc/plugins/org.polarsys.capella.ui.doc/html/04.%20User%20Interface/4.05.%20Diagram%20Editor.mediawiki#L84-L105). + +Two existing semantics prevent treating every outward drag as “delete one relation”: + +- [`effective_performer`][S8] falls back from direct Allocation to RoleAssignment/RoleRealization when that resolves to one performer. Removing a direct allocation can leave the same grouping, reveal another role-derived performer, or leave ambiguity. `allocation_summary == None` can mean mixed/unallocated leaves; it does not identify a unique old performer. +- [Allocation deletion impact][S9] can remove dependent SA PortAllocations and then ExchangeAllocations. Other references such as trace/artifact ownership may also be affected. Editing an Allocation's source preserves its identity but can invalidate existing mappings or participant scenarios. Validate the entire candidate and show real impact; never hide collateral deletion behind a successful drop. + +“Out to root” means outside a performer for this task. Source-root work with no effective performer becomes diagram-root work. A decomposed child can still remain inside its work parent. Label that destination **Unallocated — decomposition retained**, and preview actual resulting groups. Moving a child out of its semantic decomposition is a separate explicit action. + +## Interaction candidates + +Criteria: perform the user's ordinary move directly, preserve domain meaning, make consequences visible before commitment, and use one existing source/history transaction. + +| Candidate | Benefits | Tradeoffs | +| --- | --- | --- | +| Dedicated “Assign performer” tool or relationship editor; ordinary drag stays layout-only | Clear mode, straightforward target validation | Adds a mode/menu before the exact gesture the user requested; it leaves ordinary incoming/outgoing drag apparently ineffective. Keep as a keyboard/accessibility alternative, not the primary workflow. | +| **Context-sensitive body drag in Allocation views** | Incoming, between-performer and outgoing movement use one familiar gesture; same-performer movement remains arrangement | Needs stable target hit-testing, explicit preview and ambiguity handling. Recommended. | +| Geometric nesting becomes semantic containment | Superficially simple implementation | Violates work/performer ownership rules, destroys decomposition, and confuses allocation with containment. Reject. | +| Allow composite-parent Allocation directly | Makes bulk dragging appear cheap | Contradicts current source/core semantics and peer issue 4's correction. Reject. | + +### Recommended gesture + +Ordinary work-body drag in **ProjectionKind::Allocation** selects a semantic destination while moving a ghost. Other projections retain their established layout movement; an explicit Assign/Unassign action can enter the same operation when needed. Provide a discoverable **Arrange only** affordance, also keyboard accessible, so review-layout movement cannot accidentally change allocation. + +Capture the accepted document, revision, diagram, canvas epoch, placement/work ID, original geometry, selected leaf set and direct-allocation identities at press. Respect the existing screen-space threshold. Use the press-time projected performer rectangles for hit-testing, excluding the dragged work and its moving subtree: the preview's expanded old enclosure must not chase the pointer and make exit impossible. + +A compatible performer header or exposed body highlights with **Assign to Rescue team**, **Move from A to B**, or **Assign 4 leaf activities to B**. Same performer says **Arrange within B**. Exposed blank canvas outside all performer groups is the outgoing destination. A drop over an incompatible object, outside the viewport, or over unrelated overlapping performer rectangles is not an implicit root drop. Nested performers resolve to the most specific unambiguous visible performer; arbitrary paint order must not choose between overlapping peers. The pointer is the destination anchor; show the chosen destination before release, rather than infer it from how much of the dragged box overlaps. + +Hovering a work card does not reparent into that work. Ports, links, other-layer performers and hidden objects are not allocation targets. A collapsed performer may be targeted via its visible header; successful drop expands that one target in the same presentation transaction so the work does not disappear. Cancellation preserves the prior fold state. + +The preview must show **the candidate semantic grouping**, including any affected composite ancestors. For a single leaf changing allocation, do not relocate an entire target performer's existing contents. Pointer motion changes no accepted data. A valid ordinary release commits one prepared Batch; Escape, hidden/outside release, unfocus or invalid target commits none. Same allocation with changed coordinates is one layout operation; a true no-op adds no history. + +## Allocation command semantics + +Compute scope from the selected work definition's current same-kind descendant leaves. Do not use visual descendants as the semantic leaf list: folding, unplaced children and mixed performer groups can hide relevant leaves. Preserve every work parent, ID, name, alias, description, work exchange, role assignment, trace, scenario use and unrelated allocation unless a separately shown dependency repair explicitly changes it. + +| Starting state | Drop onto performer B | Drop outside performer grouping | +| --- | --- | --- | +| Leaf, no direct allocation | Create one Allocation B → leaf with a fresh stable ID allocated once during preparation. If role-derived responsibility exists, show it; B is an explicit user choice, not inferred responsibility. | No direct allocation to remove. If role-derived grouping remains, show that exact reason and offer its existing role-assignment editing path; do not pretend the work became unallocated. | +| Leaf, direct A → leaf | Edit the existing relation's source to B, retaining relation ID, alias/name/description and target. If A=B, only layout changes. | Delete that exact Allocation after impact analysis. The predicted root/group result must be truthful about any remaining role-derived responsibility. | +| Composite with all current leaves unallocated, or all with one unambiguous direct performer | Apply the preceding leaf rules to **all current leaves** in one Batch. Preview leaf count; keep the composite without direct Allocation. | Remove direct allocations across that leaf set in one Batch, subject to role and dependency checks. Do not delete the work or its decomposition. | +| Composite with mixed performers, partly assigned leaves, or role-derived responsibility | Retain a bounded local impact draft after release: show counts grouped by old performer/unallocated/role-derived status and exact destination. Explicit Apply means assign the displayed leaf set to B. | Retain a bounded draft naming which direct allocations will be removed and which role-derived responsibilities remain. Do not select an arbitrary performer or silently clear role relations. | + +Mixed context is not an invalid model, and an explicit destination can resolve it. A source draft containing duplicate direct allocations is different: it must not be accepted as a valid canvas baseline, and a stale drop must not repair it opportunistically. + +For direct moves without extra impact, no extra confirmation is needed. If standard Delete would remove IDs beyond the intended Allocation set, or a source change invalidates port/exchange/scenario dependencies, retain a local impact/refusal state with named reasons and a route to the existing repair editors. Never silently delete these dependents or invent replacement ports/channels. Root can later extend that draft to support an explicitly reviewed repair Batch. A role warning allowed by current validation remains advisory; do not turn it into a new metamodel prohibition. + +Prepare the entire semantic-plus-placement candidate before acceptance. Compute explicit moved-placement IDs and route deltas from the captured work subtree; do not issue `MoveNode(with_descendants=true)` blindly *after* allocation changes, since its newly computed visual descendants can differ. Preserve hidden subtree placement offsets where applicable. Project the candidate and use existing bounded [insertion adjustment][S11] behavior only for affected collisions; include any resulting node/route/fold edits in the same Batch. If the result still produces an unreadable giant enclosure, the preview fails acceptance and requires local arrangement work, not a misleading success message. + +Use existing CreateRelation/EditRelation/Delete/EditNode/SetEdgeRoute/Batch commands and `document.execute`; no schema change or persisted desktop-only grouping map is needed. Creation IDs are reused by retry/Redo. Exact source and Project Undo/Redo must include relations, layout, fold state and authorized impact together. API/text users retain the same semantic operations. + +## Production seams and ownership + +- **New bounded planner module**, preferably `crates/oa-desktop/src/allocation_drag.rs`: request/destination types, leaf traversal, direct-versus-role provenance, candidate/impact computation and command composition using existing core validators. Keep it independent of Iced input so behavior can be checked directly. This is UI proposal composition; existing core remains authoritative. +- **`diagram.rs`**: replace the bare movement tuple for allocation-capable work with stamped gesture state; stable destination hits, candidate projection, ghost/highlight/text and release intent. Preserve ports, connections, pan and ordinary route editing. Current [release event][S1] has no captured context. +- **Root-owned `app.rs` dispatch**, ideally a small new `app/allocation_drag.rs` handler: admit only the current clean Diagram owner, recheck captured context and exact identities, recompute the candidate against current data, then execute once. The current [MoveNode handler][S2] calls `apply()`; the new semantic drop must **not** auto-apply unrelated dirty properties or source. Reuse the [route context shape][S10], but add view/bounds/camera-lifetime invalidation where needed. +- Existing `projection.rs`, `validation.rs`, `references.rs` and `session.rs` supply semantics and geometry. No planned change to leaf validation or allocation summary. Add focused language/help wording only after root accepts the interaction. +- Coordinate any palette/menu placement with root. `model_menu.rs` dispatches Create child work through `Message::CreateContextElement(work, true)`; leave that peer-owned issue 4 correction and any helpers it introduces alone. `architecture_authoring.rs` separately handles performer-context allocated creation and does not need changes for dragging existing work. Root must keep the new App drag dispatch separate from the peer creation branch when integrating. In particular, transfer of an allocation to the first child must invalidate a previously captured leaf drag. No syntax/source-navigation ownership is acquired. + +A source refusal or stale drop retains the accepted model/source/history. A retryable impact/source failure may retain its bounded draft; after revision change, require recomputation and a renewed visible result rather than replaying old commands. Canvas errors must remain visible near the destination and in status, not depend on an offscreen Inspector. + +## OA/SA user acceptance + +Run the same fixture pattern with OA Entity/Actor + Activity and SA System/SystemActor + Function, at 1100×700 client and a wider window. These are proposed checks, not passing tests. + +1. **Incoming/outgoing leaf.** Drag a source-root unallocated leaf into A; see the exact target highlight and nested result. Save/reopen. Drag it out to blank canvas; see A release its enclosure and the leaf remain. Verify decomposition unchanged, one Allocation created then removed, and exact source/model Undo/Redo for each operation. +2. **Reassign and arrange.** Drag A's leaf into B; original Allocation UUID and metadata survive with source B. Move within B and use Arrange only across a group boundary; allocation remains unchanged. Check all other performers, work uses, routes and sibling positions. A stationary click and release on an invalid object add no history. +3. **Decompose then move.** After peer issue 4 creates first/second/grandchild work, move the uniformly allocated composite A → B. Only leaf allocations change; parent allocation remains absent, decomposition and all UUIDs survive. Move one child out: preview and final result must agree about changed ancestor summaries. A semantically nested child is not falsely advertised as source-root. +4. **Mixed and role-derived work.** Drag a composite containing A/B/unallocated leaves; the bounded impact draft shows all three categories. Cancel changes nothing; Apply to a chosen performer affects the exact displayed set. Remove direct allocation from a role-assigned leaf and verify the residual role grouping is explained. Ambiguous roles never select a performer automatically. +5. **SA technical and OA communication dependencies.** Include SA PortAllocation/ExchangeAllocation and participant-bound scenarios, plus OA exchange allocation/role cases. Retarget/remove only when the whole candidate is valid. Extra cascade or invalidated mapping is visible and retained for repair; there is no hidden dependent loss. Exact cancellation preserves every source byte. +6. **Stale and source refusal.** Press, then change owner/view/revision, resize/Fit, open an editor/dialog, begin save or create the child that retires the captured leaf allocation; deliver the old release. No mutation. Inject a source-edit/write refusal; no model/layout/history partial commit, pending unrelated edits survive, and retry uses a freshly validated candidate. Exercise Escape, unfocus and release outside the viewport with actual input. + +Native acceptance must verify target visibility, no enclosure chasing, readability, focus and correct recovery in the actual packaged build. Preserve the broader OA/SA goal; closing this specific interaction does not establish complete architecture usability or issue 2 completion. + +[S1]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/diagram.rs#L936 +[S2]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/app.rs#L2780 +[S3]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/session.rs#L771 +[S4]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/projection.rs#L81 +[S5]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/projection.rs#L541 +[S6]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/diagram.rs#L410 +[S7]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/docs/language.rst#L424 +[S8]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/validation.rs#L2850 +[S9]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/references.rs#L270 +[S10]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/route_edit.rs#L6 +[S11]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/diagram.rs#L79 diff --git a/harness/release-activity-allocation-drag-review-20260911.md b/harness/release-activity-allocation-drag-review-20260911.md new file mode 100644 index 00000000..92e377ae --- /dev/null +++ b/harness/release-activity-allocation-drag-review-20260911.md @@ -0,0 +1,157 @@ +# Independent allocation-drag proposal review — 2026-09-11 + +Codex-956atf `/root/allocation_drag_critic`, bounded source/design critique of +[the proposal](release-activity-allocation-drag-design-20260911.md) for +[issue 3](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/3). +Read local AGENTS/PROJECT_GOAL, remote `main` AGENTS, STATE, issue 3/comments and +[issue 4 coordination](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/4#issuecomment-446). +Working branch read from `.git/HEAD`: `codex/behavior-authoring-20260910`, HEAD +`06ec9621297627a1f7d5c3bc65efb237d1c9923e`. Source anchors below refer to the +files inspected in that workspace. Only this new report was written. No build, +test, native execution, production edit, Git/index change or Gitea write occurred. +This is not implementation or acceptance evidence. Root owns synthesis and integration. + +## Judgment + +The diagnosis and principal choice are sound: ordinary drag in an Allocation +view should compose allocation and placement. Existing `MoveNode` only moves +placements/routes; projected parentage comes from `allocation_summary` and +semantic ancestry. Work-to-performer `Reparent`, relaxed leaf-only validation, +or turning off enclosure would damage the existing model. Direct allocation +retargeting should preserve relation identity and metadata. Preserve separate +decomposition, effective role responsibility and actual direct allocation. + +The proposal needs the following corrections before it is precise enough to +implement. The first three can otherwise cause hidden semantic changes or a +broken ordinary gesture. + +## Material corrections + +1. **Resolve same-performer role semantics.** A leaf with `RoleAssignment R → x` + and `RoleRealization A → R` is displayed under A without direct Allocation. + The gesture text promises **Arrange within A**, but the no-direct-allocation + table says to create `A → x` on that drop. These are observably different: + direct allocation subsequently overrides role changes, and OA communication + allocation uses actual allocation rather than role responsibility. Specify + that dropping within the current unique effective performer is layout-only + for ordinary drag, including uniformly role-derived composites. Creating a + direct assignment to that same performer requires an explicit Assign action. + Dropping onto a different B may create direct allocation with the existing + advisory role warning. If removing direct A reveals role-derived B, the label + must say **Remove direct assignment; role places work in B**, not Unallocated. + A role-only outward drop should explain its retained role grouping and leave + accepted placement unchanged when it cannot fulfill the requested exit. + Sources: [effective performer](../crates/oa-core/src/validation.rs#L2850), + [role conflict is a warning](../crates/oa-core/src/validation.rs#L2553), + [actual OA communication allocation](../crates/oa-core/src/ports.rs#L1180). + +2. **Compare surviving semantic records as well as deleted IDs.** + `deletion_impact` is not a complete semantic-change report. Counterexample: + same-layer Constraint C references Allocation a in `constrained`. Deleting a + clears C's reference while retaining C; the candidate can remain valid and + the removed-ID set can be exactly `{a}`. The proposed extra-deletion gate + would approve a silent constraint edit. Compare the prepared candidate's full + semantic diff to the intended allocation edits, and expose/refuse any other + semantic mutation as well as cascaded deletion. Presentation cleanup of the + deleted relation's own route is a distinct expected effect. Full validation + remains necessary for retargeting: state availability/actions and participant + scenarios also depend on effective performer. Preserve allowed warnings; + notably OA unallocation may leave a communication mapping incomplete with a + warning, whereas SA can cascade through port and exchange allocations. + Sources: [constraint validity](../crates/oa-core/src/validation.rs#L560), + [surviving-record cleanup](../crates/oa-core/src/references.rs#L403), + [deferred deletion](../crates/oa-core/src/references.rs#L372), + [state context](../crates/oa-core/src/validation.rs#L2920). + +3. **Guard mouse-down selection, not just release.** Current node press arms + `state.drag` and publishes `SelectNode` without checking `editable`. + `SelectNode` can call `apply()` before a drop happens. Merely removing + `apply()` from the new release handler does not prevent unrelated dirty + properties from entering history, and may invalidate the captured revision + immediately. Give semantic drag a clean-owner admission/selection path at + press and recheck it at release. Dirty-source/property/behavior refusal must + not fall through to legacy movement that applies the draft. Preserve simple + selection behavior deliberately; do not silently commit a form to make drag + work. Sources: [press](../crates/oa-desktop/src/diagram.rs#L797), + [selection applies](../crates/oa-desktop/src/app.rs#L2129), + [legacy release dispatch](../crates/oa-desktop/src/app.rs#L2943). + +4. **Specify two scopes and an actual placement rule for composites.** Define + the semantic same-kind work closure/leaf set separately from the presentation + placements that move. Counterexample: mixed composite P has x allocated to A + and y to B. x and y can be projected directly under those performers, outside + P's visual subtree. Changing both allocations to C while moving only P's + captured visual descendants leaves x/y at their old distant coordinates; + C then encloses all of them. Even translating all semantic placements by one + delta preserves that excessive spread. Specify a bounded packing rule for + this converging case, or retain a truthful refusal until it exists. Capture + hidden placements too; never invent placements for unplaced leaves, and edit + only the active diagram's geometry. An outgoing child can also make its + previously uniform composite move to a different projected group: include + affected ancestor grouping in the preview. Existing insertion adjustment only + pushes colliding siblings down; it does not solve arbitrary distributed + subtree packing. Sources: [parent derivation](../crates/oa-core/src/projection.rs#L81), + [descendant movement](../crates/oa-core/src/session.rs#L771), + [insertion adjustment](../crates/oa-desktop/src/diagram.rs#L79). + +5. **Keep the target contract stable and the candidate projection fresh.** + Press-time rectangles are the right defense against enclosure chasing. Keep + their destination outline visible while candidate enclosures change, or the + visible target and accepted hit area can disagree. Define semantic ancestor + containment for nested performer precedence; geometric inclusion alone must + not turn overlapping peers into parent/child. Define the boundary/overlap and + work-card occlusion cases explicitly. Capture/invalidate bounds, camera and + view lifetime as proposed, preserving the ordered native pointer handling. + Semantic preview also requires a candidate `Project` and candidate `ViewIndex`: + the current preview clones only Diagram and reuses the accepted semantic + index. Extend scene-cache keys with semantic destination/plan identity, so an + unchanged coordinate cannot reuse a previous target's scene. Recompute model + work when destination/scope changes, rather than cloning and validating a + large project on every pointer pixel. Sources: + [existing preview/cache](../crates/oa-desktop/src/diagram.rs#L410), + [index lifetime contract](../crates/oa-core/src/projection.rs#L229). + +## Smallest coherent implementation path + +Keep one pure, bounded proposal composer plus stamped canvas gesture/App +admission. Use current commands and `Document::execute`; its source compilation +precedes its single commit and history stores exact source snapshots +([transaction](../crates/oa-desktop/src/document.rs#L301)). No metamodel change, +allocation inheritance or new generic draft framework is needed. + +First establish ordinary incoming, between-performer, outgoing and same-performer +leaf gestures, then uniform composites with explicit descendant scope. Keep +unsupported role exits, collateral semantic edits and unreadable mixed layouts +as named local refusals with existing repair navigation. This is a staged scope, +not grounds to claim every composite workflow complete. A full mixed-impact +editor and automatic dependency repair are larger capabilities and should not +block proving the user's basic direct workflow. Mixed status alone is not a +semantic error or a reason for obligatory confirmation once a safe, readable +exact-set preview exists. Extra Apply steps should resolve a real scope/impact +decision. A visible **Arrange only** alternative should be temporary or clearly +indicated, not a sticky mode users must remember after every assignment. + +## Acceptance additions before claiming the fix + +- OA and SA leaf incoming/reassign/outgoing: exact relation identities and + metadata, work parents, active-diagram positions/routes, save/reopen, and one + exact Project/source Undo/Redo step. A snapped no-op creates no history. +- Role-only same-performer arrangement creates no direct allocation. Direct + removal that reveals another role performer has the same grouping and wording + in preview and result; warnings remain advisory. +- Constraint referencing an allocation, artifact/trace attached to an allocation, + SA port/exchange dependencies, OA communication mapping and participant-bound + scenario/state behavior: no unshown semantic deletion or surviving-record edit. +- Dirty form before press, dirty source, modal/save ownership, revision change, + peer first-child allocation/role-assignment transfer, camera/bounds change, Escape/unfocus and + outside release: no hidden apply or partial history. Include the actual mounted + press → SelectNode → move → release dispatch, not only planner calls. +- Uniform folded/unplaced children and mixed P/x/y fixture: exact leaf scope, + hidden-placement handling, bounded readable grouping or explicit refusal, + unchanged other-diagram geometry, and no target/enclosure chasing. Verify + competing/overlapping/nested targets and that the shown destination is the + committed destination. + +Exercise the completed direct workflow in the actual Windows package at the +minimum client size. Planner tests and this source review cannot establish +target discoverability, cognitive load, readable layout or native event behavior. diff --git a/harness/release-behavior-canvas-union-review-20260911.md b/harness/release-behavior-canvas-union-review-20260911.md new file mode 100644 index 00000000..d446ee0a --- /dev/null +++ b/harness/release-behavior-canvas-union-review-20260911.md @@ -0,0 +1,65 @@ +# Shared behavior canvas — independent root union review + +Codex-956atf `/root/canvas_union_review`, 2026-09-11. **No actionable correctness, input-ownership, architecture or source/history regression was found in this bounded source review of the integrated canvas and scenario-parent focus paths.** This is not a build, runtime, visual or release acceptance verdict. + +## Scope and identity + +Root delegated this read-only review against the production union on `codex/behavior-authoring-20260910`, first identified by root as commit `6969c825a8340980b6a419524ade3bb687a6051f`, then frozen at `06ec9621297627a1f7d5c3bc65efb237d1c9923e`. The latter adds two test-only compiler adaptations described below. Commit identities are root-provided; this reviewer did not run Git. The scenario test freeze independently matched SHA256 `950CF4347A3EDEA615B1AC71D9A681C32B446B5109FCFD0F03F3AE2B147360BF`. + +Read the current AGENTS.md and PROJECT_GOAL.md, the [prior R1/R2/R3 review](release-behavior-canvas-r123-review-20260911.md), the isolated candidate's R1/R2/R3 source handoff, and the relevant actual root implementation and tests. The prior review's resolved counterexamples were checked for integration regressions rather than re-reported. Root owns Gitea coordination, compilation, native acceptance and publication. Only this report was written. + +The following independently observed raw working-file hashes identify the important union boundaries. Paths are relative to `crates/oa-desktop/src/`. + +| Path | SHA256 | +| --- | --- | +| `app.rs` | `AA2E4853B25BB7E185B1DE49EDA42028523C8EBF9599BFE6B7CB2A2B2A688090` | +| `behavior.rs` | `5E7CECB54BFCA64E4462F7881F460900ABD925BA2CFE9451CE2B0C78AF4BC850` | +| `behavior/scenario.rs` | `FB1DA03E8DF05A59253F7FDF34391E30ABD8AEA427B7D0A12B86B80FF3BF9ECA` | +| `behavior/scenario/focus.rs` | `73A9EA10AD7312086081B8688396A21AB4251C9FAECC2DF6D984B702F3963533` | +| `behavior/scenario/workspace.rs` | `284F5A7DA09BDC77D70C6788A091D57927150C796FFC10D786793CFF7DA4B7C6` | +| `behavior/scenario/workspace/tests.rs` | `950CF4347A3EDEA615B1AC71D9A681C32B446B5109FCFD0F03F3AE2B147360BF` | +| `behavior/canvas_workspace/focus.rs` | `CC58CB3838F546150D22FE4DD83A3FFD52504C279FEBAEB8D0B4A6A3B8A73950` | +| `behavior/canvas_workspace/tests.rs` | `D84D022574A6F1D69F203D9383502227FB9FC23605C6FB48779CA9F09BEE546D` | +| `behavior/topology/layout_tests.rs` | `A990947CB39B2FB15C21DFBC95EE16F92BF6BE749D3872C6904762B9BDE37089` | +| `behavior/form_session.rs` | `F5027FB3A5A5599A182E7D1A10DBA56DD66EB1F23A4BA4C4A1744C9A6A5B6E61` | + +## Production integration + +All source locations in this section are under `crates/oa-desktop/src/`. + +`app.rs:1041` nests the scenario parent owner root outside the canvas owner root, with both outside the actual hosted/reference overlays. Their custom owner types are distinct. Each operation ignores the other's marker, receives its own current marker before visiting focusable children, and independently checks ownership in Collect and Apply. This preserves the parent form guard while allowing the canvas operation to cover the actual nested overlay traversal. A pending scenario form makes `Scope::input_owner` return None (`behavior/canvas_workspace.rs:84`); opening its source child removes `form_mount` (`behavior/scenario.rs:509`); Review removes the parent owner (`app.rs:1495`). No competing current canvas/parent focus owner was found in these paths. + +`app.rs:1119` retains the exact current canvas Scope admission and narrow presentation whitelist. Only accepted presentation actions normalize the semantic epoch, and resulting control focus uses the post-action lifetime at `app.rs:1177`. The outer parent wrapper neither changes these epochs nor emits another focus operation. The resolved held-pan/toolbar and delayed canvas-task cases therefore retain their production guards after integration. Domain authoring still uses exact scenario context at `app.rs:1619`. + +Scenario Name uses the session-and-mount-derived dynamic ID (`behavior/scenario/workspace.rs:283`), while the scroll reset wrapper remains keyed only to editing session (`:615`). The sensor uses the mount key and sends that mount in FormShown (`:565`). The handler accepts same-owner geometry epoch changes but checks document, owner, revision, current owner and pending mount (`app.rs:1527`). Back, Use and Escape rearm the parent through `remount_form` (`behavior/scenario.rs:324`, `:696`, `:745`). None of these presentation paths replaces the raw invocation or source draft. The zero-delta axis preservation in `behavior/scenario/focus.rs` remains present. + +The Details pane now uses the production passive observer with `process = false` (`behavior/scenario/workspace.rs:595`). Its actual translation enters scenario Page scroll state, and a caller restore receives and consumes the matching token through `behavior.rs:114` and `behavior/scenario.rs:460`. `restore_caller` marks only Page pending (`behavior/scenario.rs:425`), consistent with the viewport using camera pan instead of a Scrollable. The reviewed source contains no new semantic command or source mutation in these observer/focus paths. + +The boxed workflow convention is retained (`workflow.rs:22`, `:45`), including `review_event.as_ref()` in `app/reference_authoring.rs:218`. Reference chooser availability excludes pending behavior forms (`app/reference_authoring.rs:72`), so its separate canvas guard does not create an uncovered simultaneous parent-form/chooser owner in the reviewed production path. + +## Mounted evaluator and nontrivial assertions + +Independently inspected the installed Iced sources under `.local/toolchain/cargo/registry/src/index.crates.io-1949cf8c6b5b557f/`: + +- `iced_winit-0.14.0/src/lib.rs:1214` drains App updates, rebuilds the resulting UI, then executes actions; `:1300` collects immediately ready task actions for the whole message batch and retains pending streams; `:1742` executes chained widget passes synchronously. +- `iced_runtime-0.14.0/src/user_interface.rs:539` operates both the real root and nested overlays. +- `iced_widget-0.14.2/src/scrollable.rs:540` reports the scroll translation before traversing unshifted child layout, matching the parent's translated bounds observer and reveal calculation. `sensor.rs:198` rearms on key change and emits on the subsequent redraw, including unchanged bounds. `iced_core-0.14.0/src/widget/id.rs` supports the dynamic `Id::from(String)` used by the union. + +The shared Mounted helper uses those actual UserInterface and Cache APIs, drains a complete input message batch before tasks, builds the final App view for ready actions, and executes Chain passes against that same UI (`behavior/canvas_workspace/tests.rs:140`, `:184`). Output messages form the next update cycle; pending streams are retained until completion. Unexpected native actions panic and unavailable renderer initialization fails. This is the relevant single-window widget/App ordering, not a replica of subscriptions, OS focus, native dialogs or arbitrary asynchronous scheduling. + +The reconciled parent tests preserve useful falsifiers: + +- `scenario/workspace/tests.rs:412` first focuses another real field, establishes actual parent overflow, performs the accepted task, and requires the exact Name to be the sole focus and fully visible. Mismatched mounts, revisions, owners and modal admission are separately rejected. +- `:794` obtains the real sensor task and first consumes initial focus before entering the actual child. Back, Use and Escape must retain session/raw identity, create a different mount, emit a new sensor task, and focus the parent again. A repeated FormShown must produce no task. +- `:864` retains an actual widget Action before replacement, child, Review and context transitions; it observes the resulting tree and requires the saved Collect to remain inert. `:935` similarly retains Apply across Review. This intentionally stronger retained-Apply invariant does not claim native Chain passes interleave. +- `:958` is explicitly a small synthetic missing-target test; it is not presented as a production-root modal test. The preceding production-root cases supply that coverage. +- `:1654` requests 700 pixels on the real Action form and requires the measured offset to exceed 64, preserves it while editing, then performs Cancel/Start with no intervening closed view. It checks zero offset before a redraw could rescue it through a new Name-focus task. Action and Observation share the same relevant field structure (`scenario/workspace.rs:280`, `:315`, `:476`), so replacement is not simply a switch to an empty form. Camera, raw invocation, recovery, source snapshot and history assertions remain. +- `:1773` tests same-bounds owner/document remeasurement with the same Mounted cache. `:1820` builds legitimate Details rows, requires the actual offset to be exactly 80, navigates through preselection and caller return, then requires the offset/camera restoration, consumed receipt, and subsequent pan movement. Source and revision comparisons surround navigation; existing rename history is acknowledged instead of asserting an empty history. + +The shared chooser/Review case continues to use actual nested search and Review fields (`behavior/topology/layout_tests.rs:568`). The two compiler adaptations in `06ec962` are consistent with installed APIs: explicit `Headless::screenshot` selects the trait's physical-size/scale overload (`behavior/canvas_workspace/tests.rs:268`), and the Review fixture calls the existing boxed event constructor (`behavior/topology/layout_tests.rs:726`). Neither changes evaluator ordering or production behavior. + +## Acceptance limits + +No compiler, test, renderer, native GUI, Git/index, network, Linux/WSL or VPN action was performed by this reviewer. Root reported the initial two test API errors and the committed adaptations; the corrected compiler run was still underway when this report was prepared. There is no executed pass claim here, and earlier reports establish different bytes. + +Root still needs the appropriate exact-union build/test and native acceptance evidence. In particular, operation geometry and prepared pixel assertions do not establish native focus/accessibility, clipping of transformed scene text, all modal geometry or minimum-window usability. The separately owned B4 renderer work and broader phase 1/issue 2/PROJECT_GOAL acceptance are not certified by this bounded integration review. No new blocking source finding is left for this reviewer to hand back. diff --git a/harness/release-behavior-canvas-windows-gate-20260911.md b/harness/release-behavior-canvas-windows-gate-20260911.md new file mode 100644 index 00000000..4ad12154 --- /dev/null +++ b/harness/release-behavior-canvas-windows-gate-20260911.md @@ -0,0 +1,101 @@ +# Shared behavior canvas: Windows union gate + +Codex-956atf `/root`, 2026-09-11. The integrated camera, permanent palette and +editor overlay pass **729/729 desktop tests** at source +`ff0175488ac57a92d401b00a0e4542582a82d8e9`, with a warning-free compiler and +257.42 seconds of test execution. This is a closed test checkpoint, not native +or release acceptance. The complete OA/SA goal remains unfinished. + +## Integration and review + +The reviewed isolated candidate `fb1da912c9951f33feed2fad9ad689bc46cd20d0` was +applied from common ancestor `c0e66f94` to root `638d06c`. Integration `6969c82` +retains current parent focus/session recovery, reference chooser and boxed +workflow behavior. Both scenario-parent and canvas focus owners wrap the actual +App view. Existing parent regressions were reconciled with the real Iced +UserInterface/Cache harness rather than obsolete synthetic canvas scroll views. + +The [independent union review](release-behavior-canvas-union-review-20260911.md) +found no actionable integration regression against `06ec962`. It independently +checked installed Iced message/action ordering and nested overlay operations; +its source-only verdict does not claim execution. Earlier +[R123 review](release-behavior-canvas-r123-review-20260911.md) and +[candidate source handoff](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/fb1da912c9951f33feed2fad9ad689bc46cd20d0/harness/release-behavior-canvas-r123-source-20260911.md) +retain their respective scopes. + +Candidate publication initially rejected because remote `113fb8b` contained the +user's instruction sync. Its only changes were AGENTS/PROJECT_GOAL, already +identical to root. Merging them in the isolated worktree produced `ca19ad2` with +unchanged candidate Rust sources. Normal push and exact remote readback passed; +neither history was force-pushed or overwritten. + +## Executed gates and corrections + +All commands used Rust/Cargo 1.98.1, Windows gnullvm, task-local LLVM-MinGW and +`CARGO_BUILD_JOBS=1`. Desktop is a **binary** target: use `--bin arcadia-studio`, +not `--lib`. Linux/WSL actions remain user-paused. + +| Source | Command after `cargo test --locked -p oa-desktop --bin arcadia-studio` | Result | +| --- | --- | --- | +| `06ec962` | `canvas_workspace::tests -- --test-threads=1 --nocapture` | 2 passed | +| `06ec962` | `production_ -- --test-threads=1 --nocapture` | 22 passed: all 17 new mounted cases plus 5 existing cases | +| `06ec962` | `behavior::scenario::workspace::tests:: -- --test-threads=1 --nocapture` | 21 passed, including all five parent-focus gates | +| `ff01754` | `-- --test-threads=1` | 729 passed, 0 failed/ignored/filtered; 257.42 s | +| `82fd0ac` | `guide::tests -- --test-threads=1` | 3 passed after guide copy refresh | + +These scopes overlap and must not be added together. The full test compile took +23.89 seconds. Source `06ec962` corrected two test API errors from the first +compile: explicit Headless screenshot trait selection and the existing boxed +semantic event constructor. `ff01754` narrowed the two concrete App-dependent +Scope methods to crate visibility, removing the resulting traceability visibility +warnings. All 518 full-gate inputs match Git bytes exactly. + +The initial mistaken `--lib` invocation failed before compilation. Subsequent +strict all-target Clippy at `82fd0ac` reported one collapsible focus conditional; +`abffa4b` preserves its condition and behavior with a single if. Its next Clippy +run reached the production target and found four obsolete variants: First, +Canvas scroll area, and the two Tools shelves. Therefore **strict Clippy has not +passed on this contribution**, and this record does not certify the subsequent +cleanup. Current ownership and validation belong in +[issue 2](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2). + +## Evidence and exact identity + +[Archive manifest](experiments/canvas-union-windows-20260911/manifest.json) +identifies 45 exact payloads totaling 1,479,172 bytes, including successful and +failed command logs/results, input manifests and the original 18 changed Rust +inputs used by the `06ec962` compiler before LF normalization. Manifest SHA256: +`FF470BD8160C89B1ECFECE7AAF72AA6A7F4D3D722C69F191FCA07FAC5169695F`. + +The `06ec962` raw Rust files differed from their Git blobs only in CRLF/LF; +their original bytes and both hashes are preserved. Before the full gate, those +owned files were normalized to the repository's existing LF policy. The later +518-input manifest is byte-identical to `ff01754`, SHA256 +`966B125EC8862D0E48ED98692BC3DC380F0242007EB4C018621A9FDE844562EB`. + +The immutable local full-gate test executable is 139,577,856 bytes, SHA256 +`63DE2FF7FB4C5E49C1E59378EAE24D77471AF35FADADFAEC5871956E7B2E0D8D`. +Its metadata is archived; the executable itself is not distributed in this +evidence folder. The full log is 83,584 bytes, SHA256 +`A6D2E0D39B45246D88F1F5B61117CDB0897B23A0D072238ECA7A5550495BD421`. +The task-local adjacent libunwind DLL is 190,464 bytes, SHA256 +`AA282FA47BB2C4E210CEA5E470CDBDCA58568F0BE0BC73E404D86AB9CD95EF6A`. +This records copied artifact identity, not a newly captured loaded-module trace. + +## Practical limits + +The tests exercise actual App widgets, focus operations, drawn focus borders, +camera transforms, stale gestures, caller return and exact source/history. Native +dialogs, subscriptions, OS input/focus, complete accessibility and current GUI +usability remain separate. Forced palette-overflow tests use 1100x420; they do +not prove native overflow at the application's 1100x700 minimum. Picker tests +inject cancellation at the external dialog boundary. + +Process free placement, saved scenario lane arrangement, wrapping/operand +transfer, full endpoint manipulation and the peer B4 renderer union remain +outside this camera milestone. The +[Capella comparison](release-capella-direct-manipulation-gap-20260911.md) records +those engineering distinctions. The separately adopted +[allocation drag contract](release-activity-allocation-drag-contract-v2-20260911.md) +and [review](release-activity-allocation-drag-review-20260911.md) address the user's +activity-movement issue; they are not implemented by this canvas gate. diff --git a/harness/release-capella-direct-manipulation-gap-20260911.md b/harness/release-capella-direct-manipulation-gap-20260911.md new file mode 100644 index 00000000..2d5206ea --- /dev/null +++ b/harness/release-capella-direct-manipulation-gap-20260911.md @@ -0,0 +1,94 @@ +# Capella direct-manipulation gap review — 2026-09-11 + +Codex-956atf `/root/capella_process_gap`. Advisory source review under [root's issue 2 scope](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/2#issuecomment-419); no design ratification or runtime acceptance. + +Reviewed Studio commit **638d06c4e33a1f54203b234d0b49c3ecf551d27d**, its actual desktop/core sources, the [prior native Capella benchmark](release-capella-scenario-benchmark-20260910.md), [canvas design](release-unified-behavior-canvas-design-20260911.md), and the isolated [R123 source handoff](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/fb1da912c9951f33feed2fad9ad689bc46cd20d0/harness/release-behavior-canvas-r123-source-20260911.md). Root is integrating changes while this report is written; Studio anchors below deliberately identify the published baseline. The R123 contribution changes presentation/input admission, not the persistent/domain contracts discussed here. + +Actual Gitea default branch is `main`; its AGENTS was retrieved through MCP. Issues 1/2 confirm root's integration ownership and peers' B4/lifecycle/header and staged-source/navigation ownership. This review changed only this report, with no compiler, tests, native apps, Git/index changes, Wiki changes, Linux/WSL or VPN actions. Primary Capella source was read at **5537297b071fff809285bc63989f5067d675bc52**. It is a source snapshot, not a claim that the installed Capella 7.1 binary behaves identically. + +## New evidence and distinctions + +The prior benchmark established point-created activity lifelines and an empty dragged LOOP. It did not establish wrapping existing messages or transferring operands. Source inspection now adds these distinctions: + +| Concept | Source-backed fact | Consequence for Studio | +| --- | --- | --- | +| Layout versus a domain edit | Capella exposes layout mode specifically to suppress operations that could change the semantic model, alongside automatic arrangement and pinning. [Diagram Editor][C1] | Preserve the engineer's ability to arrange a drawing confidently. A process node crossing another box or rank must not silently change precedence, decomposition, allocation or ownership. | +| Definition, use and responsibility | Capella's process/chain documentation distinguishes work involvements, involvement links and references to other chains. Allocation of activities to entities/actors and functions to system/actors/components is a separate relationship; the metamodel separately contains child functions. [Chains][C2], [Contextual allocations][C3], [FunctionalAnalysis.ecore][C4] | Four distinct operations exist: arrange a local use; move a definition under a same-kind definition; allocate work to a performer; move a local occurrence between behavioral owners. Nesting must advertise which operation it represents. A process use does not own the referenced activity/function definition. | +| Lane identity and order | Capella InstanceRole represents an instance. Its horizontal reorder handler moves entries in the scenario's `ownedInstanceRoles` list. It does not substitute represented identities. [Interaction.ecore][C5], [DoIRReorderExternalAction][C6] | Persisted view-owned x positions would be a deliberate Studio design choice. Do not call Capella's lane order wholly external to its model. In Studio, lane arrangement can remain behavior-neutral while being fully represented in source/REST. | +| Sequence order and operands | Capella's reorder service updates ordered interaction fragments and message ordering. Its scenario owns interaction fragments; CombinedFragment references operands and carries an operator, while covered lifelines are explicit references. [SequenceDiagramServices][C7], [Interaction.ecore][C5] | Studio's physically nested Fragment → Branch → Occurrence tree is a different representation. Implement Studio transfers against its own parent/list invariants; copying Capella's reorder calls would not be sufficient. A wider Studio frame cannot truthfully assert stored coverage that Studio does not have. | +| Function/activity edge endpoints | Capella involvement links point to specific function involvements, separately from the exchange they involve; sequence links have a different endpoint type. [FunctionalAnalysis.ecore][C4] | An exchange between work uses, a precedence arrow, a global work exchange, and a scenario message are distinct identities and edits. Repeated uses of one definition make this distinction observable. | + +The inspected Capella source establishes model/handler behavior, not the usability of every drag path. In particular, no new native claim is made for cross-operand drag or range wrapping. + +## Five representative engineering workflows + +Run each applicable workflow in OA and SA. The engineering tasks below extend the existing design's falsifiers rather than reopening historical bugs. + +| Workflow and intended result | Current observed capability | Remaining gap / boundary | +| --- | --- | --- | +| **1. Prepare a process/chain for a design review.** Place two work uses and a control node around a long alternate route; move one use across the computed rank; save, close, reopen and continue connecting it. | The graph is derived from sequence links/legacy order and fixed rank columns. Creation and guarded connect/reconnect exist. Drag reorder rejects a different column and produces `SetSteps`. [Graph][S1], [process gesture planners][S2] | Authored world placement is absent from this graph's input. Camera improvements do not preserve moved nodes. Movement must write presentation while leaving occurrence parents, ordered steps, guards and links unchanged. Existing layouts without stored positions still need a deterministic usable view. | +| **2. Explain who performs the work.** Show “Receive request” inside an operational entity or the system in an allocation view; separately show its subactivities/subfunctions; show two local uses in a process. | Core already distinguishes parent containment, allocation and represented Action use. Parent admission permits Activity → Activity and Function → Function, while scenario lifelines belong to scenarios. [parent rules][S3], [occurrence model][S4] | This is not evidence of missing allocation semantics. The direct-manipulation requirement is truthful nesting and a clear destination preview: “Arrange use”, “Allocate work” or “Move definition under …”. Dropping a process use over another use must not infer either allocation or decomposition. Full OA/SA acceptance still requires the architecture workflows; this report does not assess their current native usability. | +| **3. Put participants in the engineer's preferred order.** Place two scenario lifelines, name them for their roles, place two different lifelines representing the same work where admitted, and arrange the headers to minimize crossings. | Lifelines have separate IDs/represented references. Scene creation collects child lifelines and assigns uniformly spaced x positions; `Project::children` filters the element BTreeMap's ID order. [lane projection][S5], [children order][S6] | No authored lane order/spacing is consumed or persisted. Storage order should not dictate the only arrangement. Lane drag must retain each lifeline ID, its represented identity, every message endpoint and reuse binding. Changing x is not transferring an action to another lifeline. | +| **4. Refine a response sequence into alternatives.** Wrap a contiguous call/reply group in LOOP, add an alternate operand, move an entire nested fragment within its sequence, then deliberately move a selected sibling range to the other operand. | Clicked root/operand creation and same-parent reorder exist. The reorder planner explicitly rejects a different parent. Core already provides atomic `Batch`, `Reparent` and `SetSteps`, with branch ownership validation. [scenario planner][S7], [command primitives][S8], [branch validation][S9] | There is no range-wrap or explicit operand-transfer event/planner in this inspected canvas contract. This is an authoring gap over existing primitives, not proof that source/API cannot express it. Transfer must update parents and both ordered lists atomically, preserve moved UUIDs, and revalidate call/reply dominance, lifetime and reuse constraints. Copy/delete reconstruction would damage identity. | +| **5. Repair the exact communication use.** In a process, draw an existing OA interaction/SA functional exchange between two chosen work uses. In a scenario, reconnect a message to a compatible lifeline, keeping its values and call correlation. Open the underlying exchange only when changing its definition is intended. | Process direct reconnect edits only a SequenceLink. Exchange creation can produce an exchange contribution card. Crucially, `ContextualLinkPurpose::Exchange` already connects exact local or qualified Action addresses to a matching exchange, and its form creates/edits that relation. It is rendered as assembly incidence. [process planners][S2], [contextual planner][S10], [core endpoint validation][S11] | Do not report that work-to-work exchange edges are unrepresentable. The gap is a direct typed exchange/assembly gesture and endpoint manipulation using that existing contract. The scenario event enum has creation/reorder but no endpoint reconnect event. [scenario event contract][S12] Changing an occurrence endpoint must not silently edit the shared exchange or its correlated reply. | + +For workflow 5, SA functional exchange definitions retain both work endpoints and functional port identities; OA interactions use activity endpoints. Participant lifelines resolve through compatible work allocation/roles rather than being interchangeable with function ports. Studio already encodes these distinctions in [the exchange body][S13] and [representation compatibility][S14]. A valid destination preview must use those validators, not merely node kind or screen proximity. + +## Candidate approaches and smallest coherent next slice + +Criteria follow the user's goal: predictable direct editing, low cognitive load, saved graphical/textual equivalence, exact identity/history, and maintainability. + +| Candidate | Benefit | Material cost / rejection reason | +| --- | --- | --- | +| Keep rank/ID-derived arrangement; add more toolbar controls | Smallest implementation | Fails workflows 1 and 3: the chosen placement cannot be saved. Phase 1 is useful infrastructure, not this outcome. | +| Treat all dragging as semantic reorder/reparent | Reuses existing sequence commands | Wrong for a review layout; turns visual intention into behavioral edits. Also cannot express independent process x/y and lane spacing. Reject. | +| Desktop-only placement maps | Fast visible improvement | Loses source/REST parity and reliable reopen/history; two editing representations disagree. Reject. | +| **Owner-bound persisted behavior views** | Saves authored process placement and lane arrangement without changing behavioral order | Requires a reviewed core/source/API layout contract and integration with the in-flight schema work. Recommended. | + +**Recommended next slice: make workflows 1 and 3 durable together.** Add an explicitly owner-bound process/chain view and scenario view, reuse the shared camera/admission shell, and provide whole-body process movement plus horizontal lifeline-header placement/movement. Persist only meaningful fields: process occurrence position/size, and scenario lane x/width or a defined lane-order/spacing representation. Do not introduce arbitrary scenario y coordinates in this slice. + +The existing `Diagram`/`NodePlacement` machinery is a plausible home, but current [ProjectionKind][S15] lacks a process/scenario owner projection. Adding discriminated owner projections versus a specialized behavior-layout record requires core-owner review; this report does not choose a schema number or claim that every generic placement field has a valid scenario meaning. The contract must specify scope, identity, serialization, validation, delete/reidentify handling, and source/API construction. Multiple saved views must be able to arrange the same owner independently. + +For an owner without an authored view, display a deterministic derived scene immediately. Merely opening, inspecting, panning or fitting stays clean. Materialize the view with the first accepted placement edit in one history entry. Automatic fallback handles newly added/unplaced uses; an explicit Auto arrange may replace hand placement as one undoable edit. Adding a semantic edge must not silently rearrange previously placed objects. + +Repeated process references remain qualified by caller/reference path; start with moving the local reference block. A nested read-only preview must not become a placement keyed only by the shared definition UUID. Show the exact local occurrence identity in selection/details. + +After that slice, workflow 4 is a bounded domain continuation using existing commands, and workflow 5 a typed endpoint continuation using existing contextual/communication validation. If the core layout contract cannot proceed immediately because of shared schema ownership, designing the atomic wrap/transfer planner is useful non-overlapping work; do not substitute a temporary persisted format. Additional Capella fragment operators/coverage semantics remain a separate capability assessment, not silently excluded from the full goal by this four-operator UI. + +## Concrete acceptance and counterexamples + +These are proposed gates, **not executed results**. Check whole accepted Project plus exact source/history, not only one position field. Exercise actual pointer/keyboard workflows at 1100×700 client and a larger window; readback assertions and screenshots alone do not establish editing usability. + +| Case | Positive acceptance | Negative / invariant | +| --- | --- | --- | +| Process authored placement | Move a work-use body across at least two former rank columns, connect it, save/reopen, and observe the authored position. One release creates one layout history entry; Undo/Redo restores complete before/after states. | No step reorder, link/guard/parent/allocation change, implicit legacy-path conversion or shared-definition edit. A click below drag threshold adds no history. | +| View scope and identity | Arrange two saved views of the same owner differently; move two distinct references to the same process independently. Add a new contribution without displacing existing authored positions. | Wrong-owner/layer placements, duplicate placement identities, invalid dimensions/nonfinite coordinates, or bare nested-definition targets are rejected without partial writes. | +| Lane arrangement | Place a new lifeline at a previewed x; move its header past another lane; save/reopen. Same represented target on two lifelines retains two independently arranged lifeline identities where the scenario admits it. | Message senders/receivers, Action/State lifelines, branch steps, representations and ScenarioUse bindings remain identical. Alias/name edits must not reset authored arrangement. | +| Clean/invalid/stale operations | Open a legacy file and pan/Fit without source change. Cancel placement or encounter source-write failure while preserving the accepted model and usable retry state. | Release retained across owner/document/revision/view change, pending source draft or invalidated input lifetime commits nothing. Schema/source/API load/save preserve the same layout; invalid incoming layout is not silently discarded. | +| Wrapping/transfer continuation | Wrapping sibling IDs `[b,c]` within `[a,b,c,d]` yields `[a,fragment,d]`; its operand owns the original b/c IDs in order. Explicit transfer updates both lists/parents in one Batch, and moving a fragment carries its full subtree. | Refuse a partial child fragment, cross-parent range, ancestor into descendant, stale destination, or reply-before-call/branch/lifetime-invalid result. Ordinary drag crossing an operand does not silently transfer. All unrelated siblings, conditions, values and correlations remain identical. | +| Endpoint continuation | Drawing assembly from exact Action A to Action B offers matching exchanges; repeated definitions retain the chosen occurrence addresses. A compatible scenario endpoint edit preserves occurrence ID and all untouched payload fields. | Incompatible work/port/layer/represented participant, wrong message direction or broken call/reply correlation leaves the original intact with an actionable explanation. An assembly edit never creates a precedence relation, and occurrence repair never changes the global exchange implicitly. | + +The release remains subject to the full [PROJECT_GOAL](../PROJECT_GOAL.md): OA/SA capability coverage, transitions, source/API parity, guidance, realistic failure recovery, platform verification and independent integrated acceptance. This report identifies the next useful slice and its semantic boundaries; it does not close issue 2 or ratify the whole product. + +[C1]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/doc/plugins/org.polarsys.capella.ui.doc/html/04.%20User%20Interface/4.05.%20Diagram%20Editor.mediawiki#L84-L105 +[C2]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/doc/plugins/org.polarsys.capella.ui.doc/html/05.%20Diagram%20Management/5.5.%20Creation%20of%20Functional%20Chains%20and%20Physical%20Paths.mediawiki +[C3]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/doc/plugins/org.polarsys.capella.ui.doc/html/07.%20Modelling%20Wizards%20and%20Accelerators/7.2.%20Contextual%20Allocations.mediawiki#L10-L34 +[C4]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/m2/plugins/org.polarsys.capella.core.data.def/model/FunctionalAnalysis.ecore#L3381-L3469 +[C5]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/m2/plugins/org.polarsys.capella.core.data.def/model/Interaction.ecore +[C6]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/core/plugins/org.polarsys.capella.core.sirius.analysis/src/org/polarsys/capella/core/sirius/analysis/actions/extensions/DoIRReorderExternalAction.java#L30-L54 +[C7]: https://github.com/eclipse-capella/capella/blob/5537297b071fff809285bc63989f5067d675bc52/core/plugins/org.polarsys.capella.core.sirius.analysis/src/org/polarsys/capella/core/sirius/analysis/SequenceDiagramServices.java#L163-L291 +[S1]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/behavior/topology.rs#L988 +[S2]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/behavior/topology/direct.rs#L340 +[S3]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/validation.rs#L92 +[S4]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/model.rs#L550 +[S5]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/behavior.rs#L1304 +[S6]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/model.rs#L1828 +[S7]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/behavior/scenario.rs#L1067 +[S8]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/session.rs#L583 +[S9]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/validation.rs#L2284 +[S10]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/behavior/topology/contextual.rs#L149 +[S11]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/topology_context.rs#L269 +[S12]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-desktop/src/behavior/scenario.rs#L98 +[S13]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/model.rs#L1117 +[S14]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/validation.rs#L1284 +[S15]: http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/638d06c4e33a1f54203b234d0b49c3ecf551d27d/crates/oa-core/src/model.rs#L1431