From 4b6b5fc1977523e8cf52fdd5503a0a71c87a3f63 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 6 Sep 2026 21:49:04 -0700 Subject: [PATCH] Complete source 6 engineering workflows and preserve release validation evidence --- .gitattributes | 6 + Cargo.lock | 1 + README.md | 18 +- crates/oa-core/Cargo.toml | 3 + crates/oa-core/examples/generate_release1.rs | 53 + .../oa-core/examples/release_dense_views.rs | 441 ++ crates/oa-core/src/demo.rs | 1 + crates/oa-core/src/language.rs | 30 +- .../oa-core/src/language/semantic_source.rs | 4 + crates/oa-core/src/lib.rs | 1 + crates/oa-core/src/migration_v4.rs | 4 + crates/oa-core/src/model.rs | 49 +- crates/oa-core/src/persistence.rs | 175 +- crates/oa-core/src/ports.rs | 48 +- crates/oa-core/src/projection.rs | 113 +- crates/oa-core/src/references.rs | 24 +- crates/oa-core/src/routing.rs | 21 +- crates/oa-core/src/semantic/catalog.rs | 21 +- crates/oa-core/src/semantic/historical.rs | 1 + crates/oa-core/src/semantic/mutate.rs | 7 +- crates/oa-core/src/semantic/review.rs | 13 +- crates/oa-core/src/semantic/types.rs | 2 + crates/oa-core/src/source_files.rs | 711 ++- crates/oa-core/src/transitions.rs | 125 +- .../oa-core/src/transitions/communication.rs | 210 + crates/oa-core/src/validation.rs | 188 +- crates/oa-core/src/validation/coverage.rs | 113 +- crates/oa-core/src/view_options.rs | 711 +++ crates/oa-core/src/view_options_tests.rs | 620 +++ .../tests/communication_boundary_scenarios.rs | 248 + crates/oa-core/tests/communication_means.rs | 515 ++ crates/oa-core/tests/language.rs | 37 +- crates/oa-core/tests/port_migration.rs | 14 +- crates/oa-core/tests/port_source.rs | 5 +- crates/oa-core/tests/r3_constructor_ack.rs | 5 +- crates/oa-core/tests/r3_migration.rs | 18 +- crates/oa-core/tests/r3_source.rs | 2 +- crates/oa-core/tests/r6_migration.rs | 11 +- crates/oa-core/tests/r6_source.rs | 2 +- crates/oa-core/tests/release1_teaching.rs | 256 + crates/oa-core/tests/release_ports.rs | 48 +- crates/oa-core/tests/release_routing.rs | 1 + .../tests/semantic_artifact_absence_oracle.rs | 140 + .../tests/semantic_attachments_gui_oracle.rs | 398 ++ crates/oa-core/tests/source6_view_contract.rs | 409 ++ crates/oa-core/tests/source_project.rs | 12 +- crates/oa-core/tests/state_diagrams.rs | 1 + crates/oa-desktop/src/app.rs | 190 +- crates/oa-desktop/src/app/checkpoints.rs | 30 +- .../src/app/diagram_selection_tests.rs | 150 + .../src/app/review_navigation_tests.rs | 184 + .../src/app/transition_context_tests.rs | 298 ++ crates/oa-desktop/src/app/view_copy_tests.rs | 222 + crates/oa-desktop/src/behavior.rs | 4 +- crates/oa-desktop/src/behavior/scenario.rs | 140 +- .../oa-desktop/src/behavior/state_canvas.rs | 1 + crates/oa-desktop/src/behavior/state_edit.rs | 1 + crates/oa-desktop/src/behavior/state_menu.rs | 1 + crates/oa-desktop/src/connection_tool.rs | 49 +- crates/oa-desktop/src/diagram.rs | 26 +- .../oa-desktop/src/diagram/lifecycle_tests.rs | 1 + .../oa-desktop/src/diagram/pointer_tests.rs | 1 + .../oa-desktop/src/diagram/port_hit_tests.rs | 1 + crates/oa-desktop/src/diagram/port_labels.rs | 6 +- .../src/diagram/view_options_tests.rs | 89 + crates/oa-desktop/src/document.rs | 43 + crates/oa-desktop/src/forms.rs | 16 +- crates/oa-desktop/src/forms/data_fields.rs | 9 +- crates/oa-desktop/src/forms/view_fields.rs | 645 +++ crates/oa-desktop/src/guide.rs | 329 +- crates/oa-desktop/src/home.rs | 35 + crates/oa-desktop/src/home/tests.rs | 37 + crates/oa-desktop/src/main.rs | 1 + crates/oa-desktop/src/model_menu.rs | 6 + crates/oa-desktop/src/port_workflow.rs | 13 + crates/oa-desktop/src/recovery.rs | 20 +- crates/oa-desktop/src/recovery/source5.rs | 133 + .../oa-desktop/src/recovery/source5_tests.rs | 296 ++ crates/oa-desktop/src/semantic_review.rs | 849 +--- .../src/semantic_review/artifacts.rs | 615 +++ .../artifacts_recovery_falsifier_tests.rs | 328 ++ .../src/semantic_review/artifacts_tests.rs | 634 +++ .../src/semantic_review/artifacts_view.rs | 147 + .../src/semantic_review/attachments.rs | 456 ++ .../src/semantic_review/attachments_tests.rs | 795 +++ .../src/semantic_review/attachments_view.rs | 79 + .../oa-desktop/src/semantic_review/batch.rs | 689 +++ .../src/semantic_review/batch_tests.rs | 807 +++ .../oa-desktop/src/semantic_review/compose.rs | 626 +++ .../compose_diagnostic_tests.rs | 302 ++ .../src/semantic_review/constructor_tests.rs | 106 +- .../oa-desktop/src/semantic_review/history.rs | 60 +- .../oa-desktop/src/semantic_review/keyed.rs | 20 +- .../src/semantic_review/keyed_rows.rs | 2 +- .../src/semantic_review/keyed_rows_tests.rs | 193 +- .../src/semantic_review/keyed_tests.rs | 88 +- .../src/semantic_review/message_kind_tests.rs | 49 +- .../oa-desktop/src/semantic_review/ordered.rs | 2 +- .../src/semantic_review/ordered_tests.rs | 103 +- .../src/semantic_review/recovery.rs | 394 +- crates/oa-desktop/src/semantic_review/row.rs | 887 ++++ .../oa-desktop/src/semantic_review/scalar.rs | 10 +- .../oa-desktop/src/semantic_review/tests.rs | 79 +- crates/oa-desktop/src/semantic_review/view.rs | 362 +- .../view_reserved_artifact_tests.rs | 118 + crates/oa-desktop/src/source_workspace.rs | 16 + crates/oa-desktop/src/view_copy.rs | 237 + crates/oa-desktop/src/views.rs | 43 +- crates/oa-desktop/src/workflow.rs | 63 +- .../oa-desktop/src/workflow/communication.rs | 304 ++ crates/oa-desktop/src/workflow/recovery.rs | 59 +- crates/oa-server/src/routes.rs | 52 +- crates/oa-server/tests/http.rs | 6 +- .../tests/http/communication_limits.rs | 119 + crates/oa-server/tests/http/data_contracts.rs | 15 +- crates/oa-server/tests/http/source6_views.rs | 374 ++ crates/oa-server/tests/semantic_schema.py | 131 +- docs/api.rst | 60 +- docs/language.rst | 116 +- docs/openapi.json | 458 +- docs/scope.md | 25 +- examples/release1/README.md | 86 + examples/release1/evidence/repair-check.md | 15 + examples/release1/expected-diagnostics.json | 9 + examples/release1/model.json | 4401 +++++++++++++++++ examples/release1/model.rs | 1037 ++++ .../activity_oa_restore_work.arcadia | 45 + .../process_oa_assessment_procedure.arcadia | 25 + .../process_oa_field_procedure.arcadia | 67 + .../oa/behavior/process_oa_response.arcadia | 128 + .../behavior/scenario_oa_conversation.arcadia | 64 + .../scenario_oa_full_scenario.arcadia | 41 + .../behavior/state_machine_oa_monitor.arcadia | 80 + examples/release1/oa/capabilities.arcadia | 27 + examples/release1/oa/model.arcadia | 24 + examples/release1/oa/participants.arcadia | 60 + examples/release1/oa/types.arcadia | 35 + examples/release1/project.arcadia | 33 + .../process_sa_assessment_procedure.arcadia | 25 + .../process_sa_field_procedure.arcadia | 67 + .../sa/behavior/process_sa_response.arcadia | 128 + .../behavior/scenario_sa_conversation.arcadia | 73 + .../scenario_sa_full_scenario.arcadia | 41 + .../behavior/state_machine_sa_monitor.arcadia | 80 + examples/release1/sa/capabilities.arcadia | 27 + .../function_sa_audit_thresholds.arcadia | 5 + .../function_sa_restore_work.arcadia | 93 + examples/release1/sa/model.arcadia | 24 + examples/release1/sa/participants.arcadia | 126 + examples/release1/sa/types.arcadia | 50 + examples/release1/traceability.arcadia | 1156 +++++ examples/release1/transition-before.json | 1198 +++++ examples/release1/transition-request.json | 114 + .../views/diagram_oa_allocation_view.arcadia | 100 + .../views/diagram_sa_allocation_view.arcadia | 100 + .../views/diagram_sa_capability_view.arcadia | 72 + .../views/diagram_sa_service_view.arcadia | 104 + .../views/diagram_sa_state_view.arcadia | 90 + harness/DECISIONS.md | 14 + harness/PLAN.md | 8 + harness/STATE.md | 152 +- harness/TASK_LEDGER.md | 168 +- .../interface-operation-probe/README.md | 41 + .../interface-operation-probe/hashes.json | 14 + .../output/after-reset-deletion.arcadia | 138 + .../output/after-reset-deletion.json | 276 ++ .../output/boundary-only.arcadia | 155 + .../output/boundary-only.json | 310 ++ .../explicit-functional-refinement.arcadia | 275 + .../explicit-functional-refinement.json | 552 +++ .../interface-operation-probe/probe.rs | 335 ++ harness/fixtures/release1-native/README.md | 20 +- .../release1-native/artifacts/README.md | 38 + .../artifacts/generate-semantic-artifacts.rs | 263 + .../semantic-artifacts-create.after.arcadia | 202 + .../semantic-artifacts-create.after.json | 244 + .../semantic-artifacts-create.arcadia | 43 + .../artifacts/semantic-artifacts-create.json | 76 + .../semantic-artifacts-create.manifest.json | 22 + .../semantic-artifacts-create.request.json | 147 + .../semantic-artifacts-delete.after.arcadia | 190 + .../semantic-artifacts-delete.after.json | 219 + .../semantic-artifacts-delete.arcadia | 52 + .../artifacts/semantic-artifacts-delete.json | 87 + .../semantic-artifacts-delete.manifest.json | 20 + .../semantic-artifacts-delete.request.json | 49 + .../semantic-artifacts-move.after.arcadia | 115 + .../semantic-artifacts-move.after.json | 154 + .../artifacts/semantic-artifacts-move.arcadia | 52 + .../artifacts/semantic-artifacts-move.json | 87 + .../semantic-artifacts-move.manifest.json | 23 + .../semantic-artifacts-move.request.json | 87 + .../artifacts/semantic-artifacts-native.md | 37 + .../artifacts/semantic-artifacts.hashes.json | 26 + .../communication-means/README.md | 220 + .../cm-create.after-template/entry.arcadia | 244 + .../cm-create.after-template/expected.json | 370 ++ .../traceability.arcadia | 19 + .../cm-create.request.json | 56 + .../communication-means/cm-draw.arcadia | 210 + .../communication-means/cm-draw.json | 301 ++ .../cm-reuse.after-template/entry.arcadia | 230 + .../cm-reuse.after-template/expected.json | 348 ++ .../traceability.arcadia | 19 + .../communication-means/cm-reuse.request.json | 51 + .../cm-reverse-scenario.arcadia | 320 ++ .../expected.json | 679 +++ .../oa/behavior/scenario_oa_cm_200.arcadia | 63 + .../request.json | 47 + .../traceability.arcadia | 91 + .../entry.arcadia | 396 ++ .../expected.json | 749 +++ .../oa/behavior/scenario_oa_cm_200.arcadia | 63 + .../traceability.arcadia | 91 + .../cm-reverse-scenario.arcadia | 299 ++ .../cm-reverse-scenario.json | 457 ++ .../cm-reverse-scenario.request.json | 47 + .../cm-transition-ready.arcadia | 218 + .../cm-transition-ready.json | 315 ++ .../generate-communication-means.rs | 874 ++++ .../communication-means/hashes.json | 32 + .../communication-means/manifest.json | 33 + .../dense-40/dense-40-oracle.json | 954 ++++ .../release1-native/dense-40/dense-40.arcadia | 956 ++++ .../release1-native/dense-40/dense-40.json | 1818 +++++++ .../semantic-attachments-native.md | 11 + .../semantic-attachments.after.arcadia | 223 + .../semantic-attachments.after.json | 346 ++ .../semantic-attachments.arcadia | 168 + .../semantic-attachments.hashes.json | 22 + .../release1-native/semantic-attachments.json | 287 ++ .../semantic-attachments.manifest.json | 36 + .../semantic-attachments.request.json | 108 + .../release1-native/semantic-batch-native.md | 11 + .../semantic-batch.after.arcadia | 107 + .../release1-native/semantic-batch.after.json | 153 + .../release1-native/semantic-batch.arcadia | 61 + .../semantic-batch.hashes.json | 32 + .../release1-native/semantic-batch.json | 103 + .../semantic-batch.manifest.json | 25 + .../semantic-batch.request.json | 88 + .../release1-native/semantic-keyed.steps.md | 2 +- .../15000000-0000-4000-8000-000000000010.json | 38 + .../source5-recovery/generate.py | 34 + harness/fixtures/rest-coverage/README.md | 2 +- .../README.md | 12 + .../STATE.md | 207 + .../TASK_LEDGER.md | 192 + harness/native-release1-review-check.rs | 107 + harness/probes/chain_context_resolution.rs | 437 ++ ...ct-recovery-independent-review-20260906.md | 37 + harness/release-artifacts-native-20260906.md | 78 + .../release-attachments-native-20260906.md | 41 + ...se-behavior-transition-context-20260906.md | 41 + ...-transition-independent-review-20260906.md | 59 + ...ly-contract-independent-review-20260906.md | 126 + ...-chain-assembly-contract-probe-20260906.md | 149 + ...in-assembly-independent-review-20260906.md | 77 + ...hain-context-analysis-contract-20260906.md | 141 + ...-checkpoint-integration-review-20260906.md | 27 + .../release-communication-native-20260906.md | 69 + .../release-completeness-audit-20260906.md | 51 + .../release-current-native-verification.md | 150 + ...elease-engineering-guide-audit-20260906.md | 81 + ...elease-final-native-priorities-20260906.md | 64 + harness/release-guide-native-20260906.md | 13 + ...e-interface-operation-contract-20260906.md | 147 + ...on-contract-independent-review-20260906.md | 39 + ...ase-journal-publication-review-20260906.md | 33 + harness/release-linux-journal-publication.md | 101 + ...ase-rest-coverage-verification-20260906.md | 2 +- .../release-scope-reconciliation-20260906.md | 162 + .../release-selective-view-core-20260906.md | 78 + ...antic-artifact-boundary-review-20260906.md | 20 + harness/release-semantic-artifacts.md | 42 + harness/release-semantic-attachments.md | 38 + .../release-semantic-batch-implementation.md | 78 + ...-semantic-conflict-diagnostics-20260906.md | 71 + ...ase-semantic-review-navigation-20260906.md | 27 + ...urce5-checkpoint-compatibility-20260906.md | 33 + ...-checkpoint-independent-review-20260906.md | 16 + ...elease-source5-native-recovery-20260906.md | 44 + ...e-source7-integration-decision-20260906.md | 29 + ...lease-staged-checkpoint-review-20260906.md | 32 + ...se-teaching-independent-review-20260906.md | 34 + harness/release-teaching-native-20260906.md | 31 + .../release-timed-bundle-oracle-20260906.md | 2 +- ...se-view-duplication-dense-plan-20260906.md | 132 + harness/release-walkthrough-hook-20260906.md | 27 + ...hrough-hook-independent-review-20260906.md | 26 + 290 files changed, 48271 insertions(+), 1791 deletions(-) create mode 100644 crates/oa-core/examples/generate_release1.rs create mode 100644 crates/oa-core/examples/release_dense_views.rs create mode 100644 crates/oa-core/src/transitions/communication.rs create mode 100644 crates/oa-core/src/view_options.rs create mode 100644 crates/oa-core/src/view_options_tests.rs create mode 100644 crates/oa-core/tests/communication_boundary_scenarios.rs create mode 100644 crates/oa-core/tests/communication_means.rs create mode 100644 crates/oa-core/tests/release1_teaching.rs create mode 100644 crates/oa-core/tests/semantic_artifact_absence_oracle.rs create mode 100644 crates/oa-core/tests/semantic_attachments_gui_oracle.rs create mode 100644 crates/oa-core/tests/source6_view_contract.rs create mode 100644 crates/oa-desktop/src/app/diagram_selection_tests.rs create mode 100644 crates/oa-desktop/src/app/review_navigation_tests.rs create mode 100644 crates/oa-desktop/src/app/transition_context_tests.rs create mode 100644 crates/oa-desktop/src/app/view_copy_tests.rs create mode 100644 crates/oa-desktop/src/diagram/view_options_tests.rs create mode 100644 crates/oa-desktop/src/forms/view_fields.rs create mode 100644 crates/oa-desktop/src/recovery/source5.rs create mode 100644 crates/oa-desktop/src/recovery/source5_tests.rs create mode 100644 crates/oa-desktop/src/semantic_review/artifacts.rs create mode 100644 crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs create mode 100644 crates/oa-desktop/src/semantic_review/artifacts_tests.rs create mode 100644 crates/oa-desktop/src/semantic_review/artifacts_view.rs create mode 100644 crates/oa-desktop/src/semantic_review/attachments.rs create mode 100644 crates/oa-desktop/src/semantic_review/attachments_tests.rs create mode 100644 crates/oa-desktop/src/semantic_review/attachments_view.rs create mode 100644 crates/oa-desktop/src/semantic_review/batch.rs create mode 100644 crates/oa-desktop/src/semantic_review/batch_tests.rs create mode 100644 crates/oa-desktop/src/semantic_review/compose.rs create mode 100644 crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs create mode 100644 crates/oa-desktop/src/semantic_review/row.rs create mode 100644 crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs create mode 100644 crates/oa-desktop/src/view_copy.rs create mode 100644 crates/oa-desktop/src/workflow/communication.rs create mode 100644 crates/oa-server/tests/http/communication_limits.rs create mode 100644 crates/oa-server/tests/http/source6_views.rs create mode 100644 examples/release1/README.md create mode 100644 examples/release1/evidence/repair-check.md create mode 100644 examples/release1/expected-diagnostics.json create mode 100644 examples/release1/model.json create mode 100644 examples/release1/model.rs create mode 100644 examples/release1/oa/activities/activity_oa_restore_work.arcadia create mode 100644 examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia create mode 100644 examples/release1/oa/behavior/process_oa_field_procedure.arcadia create mode 100644 examples/release1/oa/behavior/process_oa_response.arcadia create mode 100644 examples/release1/oa/behavior/scenario_oa_conversation.arcadia create mode 100644 examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia create mode 100644 examples/release1/oa/behavior/state_machine_oa_monitor.arcadia create mode 100644 examples/release1/oa/capabilities.arcadia create mode 100644 examples/release1/oa/model.arcadia create mode 100644 examples/release1/oa/participants.arcadia create mode 100644 examples/release1/oa/types.arcadia create mode 100644 examples/release1/project.arcadia create mode 100644 examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia create mode 100644 examples/release1/sa/behavior/process_sa_field_procedure.arcadia create mode 100644 examples/release1/sa/behavior/process_sa_response.arcadia create mode 100644 examples/release1/sa/behavior/scenario_sa_conversation.arcadia create mode 100644 examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia create mode 100644 examples/release1/sa/behavior/state_machine_sa_monitor.arcadia create mode 100644 examples/release1/sa/capabilities.arcadia create mode 100644 examples/release1/sa/functions/function_sa_audit_thresholds.arcadia create mode 100644 examples/release1/sa/functions/function_sa_restore_work.arcadia create mode 100644 examples/release1/sa/model.arcadia create mode 100644 examples/release1/sa/participants.arcadia create mode 100644 examples/release1/sa/types.arcadia create mode 100644 examples/release1/traceability.arcadia create mode 100644 examples/release1/transition-before.json create mode 100644 examples/release1/transition-request.json create mode 100644 examples/release1/views/diagram_oa_allocation_view.arcadia create mode 100644 examples/release1/views/diagram_sa_allocation_view.arcadia create mode 100644 examples/release1/views/diagram_sa_capability_view.arcadia create mode 100644 examples/release1/views/diagram_sa_service_view.arcadia create mode 100644 examples/release1/views/diagram_sa_state_view.arcadia create mode 100644 harness/fixtures/interface-operation-probe/README.md create mode 100644 harness/fixtures/interface-operation-probe/hashes.json create mode 100644 harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia create mode 100644 harness/fixtures/interface-operation-probe/output/after-reset-deletion.json create mode 100644 harness/fixtures/interface-operation-probe/output/boundary-only.arcadia create mode 100644 harness/fixtures/interface-operation-probe/output/boundary-only.json create mode 100644 harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia create mode 100644 harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json create mode 100644 harness/fixtures/interface-operation-probe/probe.rs create mode 100644 harness/fixtures/release1-native/artifacts/README.md create mode 100644 harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md create mode 100644 harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json create mode 100644 harness/fixtures/release1-native/communication-means/README.md create mode 100644 harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-create.request.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-draw.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-draw.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reuse.request.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json create mode 100644 harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia create mode 100644 harness/fixtures/release1-native/communication-means/cm-transition-ready.json create mode 100644 harness/fixtures/release1-native/communication-means/generate-communication-means.rs create mode 100644 harness/fixtures/release1-native/communication-means/hashes.json create mode 100644 harness/fixtures/release1-native/communication-means/manifest.json create mode 100644 harness/fixtures/release1-native/dense-40/dense-40-oracle.json create mode 100644 harness/fixtures/release1-native/dense-40/dense-40.arcadia create mode 100644 harness/fixtures/release1-native/dense-40/dense-40.json create mode 100644 harness/fixtures/release1-native/semantic-attachments-native.md create mode 100644 harness/fixtures/release1-native/semantic-attachments.after.arcadia create mode 100644 harness/fixtures/release1-native/semantic-attachments.after.json create mode 100644 harness/fixtures/release1-native/semantic-attachments.arcadia create mode 100644 harness/fixtures/release1-native/semantic-attachments.hashes.json create mode 100644 harness/fixtures/release1-native/semantic-attachments.json create mode 100644 harness/fixtures/release1-native/semantic-attachments.manifest.json create mode 100644 harness/fixtures/release1-native/semantic-attachments.request.json create mode 100644 harness/fixtures/release1-native/semantic-batch-native.md create mode 100644 harness/fixtures/release1-native/semantic-batch.after.arcadia create mode 100644 harness/fixtures/release1-native/semantic-batch.after.json create mode 100644 harness/fixtures/release1-native/semantic-batch.arcadia create mode 100644 harness/fixtures/release1-native/semantic-batch.hashes.json create mode 100644 harness/fixtures/release1-native/semantic-batch.json create mode 100644 harness/fixtures/release1-native/semantic-batch.manifest.json create mode 100644 harness/fixtures/release1-native/semantic-batch.request.json create mode 100644 harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json create mode 100644 harness/fixtures/release1-native/source5-recovery/generate.py create mode 100644 harness/history/2026-09-06-ledgers-before-consolidation/README.md create mode 100644 harness/history/2026-09-06-ledgers-before-consolidation/STATE.md create mode 100644 harness/history/2026-09-06-ledgers-before-consolidation/TASK_LEDGER.md create mode 100644 harness/native-release1-review-check.rs create mode 100644 harness/probes/chain_context_resolution.rs create mode 100644 harness/release-artifact-recovery-independent-review-20260906.md create mode 100644 harness/release-artifacts-native-20260906.md create mode 100644 harness/release-attachments-native-20260906.md create mode 100644 harness/release-behavior-transition-context-20260906.md create mode 100644 harness/release-behavior-transition-independent-review-20260906.md create mode 100644 harness/release-chain-assembly-contract-independent-review-20260906.md create mode 100644 harness/release-chain-assembly-contract-probe-20260906.md create mode 100644 harness/release-chain-assembly-independent-review-20260906.md create mode 100644 harness/release-chain-context-analysis-contract-20260906.md create mode 100644 harness/release-checkpoint-integration-review-20260906.md create mode 100644 harness/release-communication-native-20260906.md create mode 100644 harness/release-completeness-audit-20260906.md create mode 100644 harness/release-engineering-guide-audit-20260906.md create mode 100644 harness/release-final-native-priorities-20260906.md create mode 100644 harness/release-guide-native-20260906.md create mode 100644 harness/release-interface-operation-contract-20260906.md create mode 100644 harness/release-interface-operation-contract-independent-review-20260906.md create mode 100644 harness/release-journal-publication-review-20260906.md create mode 100644 harness/release-linux-journal-publication.md create mode 100644 harness/release-scope-reconciliation-20260906.md create mode 100644 harness/release-selective-view-core-20260906.md create mode 100644 harness/release-semantic-artifact-boundary-review-20260906.md create mode 100644 harness/release-semantic-artifacts.md create mode 100644 harness/release-semantic-attachments.md create mode 100644 harness/release-semantic-batch-implementation.md create mode 100644 harness/release-semantic-conflict-diagnostics-20260906.md create mode 100644 harness/release-semantic-review-navigation-20260906.md create mode 100644 harness/release-source5-checkpoint-compatibility-20260906.md create mode 100644 harness/release-source5-checkpoint-independent-review-20260906.md create mode 100644 harness/release-source5-native-recovery-20260906.md create mode 100644 harness/release-source7-integration-decision-20260906.md create mode 100644 harness/release-staged-checkpoint-review-20260906.md create mode 100644 harness/release-teaching-independent-review-20260906.md create mode 100644 harness/release-teaching-native-20260906.md create mode 100644 harness/release-view-duplication-dense-plan-20260906.md create mode 100644 harness/release-walkthrough-hook-20260906.md create mode 100644 harness/release-walkthrough-hook-independent-review-20260906.md diff --git a/.gitattributes b/.gitattributes index cff6285e..ead553c1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,9 @@ *.png binary *.jpg binary *.zip binary + +# Exact source/recovery oracles and archived ledgers retain their measured bytes. +# Keep ordinary text diffs; only disable checkout/check-in newline conversion. +harness/fixtures/release1-native/** -text +harness/fixtures/interface-operation-probe/** -text +harness/history/2026-09-06-ledgers-before-consolidation/** -text diff --git a/Cargo.lock b/Cargo.lock index 30921add..f2f10340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2143,6 +2143,7 @@ dependencies = [ name = "oa-core" version = "0.2.0" dependencies = [ + "libc", "same-file", "serde", "serde_json", diff --git a/README.md b/README.md index 9a142008..6d4e361d 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,12 @@ The desktop starts at **Home**, where you can create a model, open a local proje The example opens in an architecture view with work nested inside its performers. Each layer also has dedicated functional dataflow and mission/capability views. Regenerate both example formats with matching identities using `cargo run --locked -p oa-core --example generate_demo`; the command verifies both saved models. Historical migration fixtures remain separate. -1. In **Operational Analysis**, create entities, actors, activities and capabilities. Use **In selected** for semantic decomposition and **Allocated** for new leaf work performed by a participant. Activities can decompose independently of their performer; roles capture shared operational responsibility. +1. In **Operational Analysis**, create entities, actors, activities and capabilities. Use **In selected** for semantic decomposition and **Allocated** for new leaf work performed by a participant. Activities can decompose independently of their performer; roles capture shared operational responsibility. **Communication means** connects operational Entities or Actors directly, with direction and carried items; **Exchange allocation** associates the Interactions it carries. Operational communication requires no system ports. 2. Switch to **System Analysis**. Define one System of Interest and external Actors, then functions, missions, capabilities and functional chains. The system boundary has no internal LA/PA components in this version. 3. Select a System or System Actor and choose **Add port** in the browser or Properties. Choose Standard or Flow, then **Create port**. Use **Boundary exchange** in the diagram palette, or **Draw boundary exchange** on a selected visible source port, to connect compatible component ports directly. Create the other participant's matching port first if none exists. Functions expose separate input/output port actions; the **Functional exchange** palette tool connects them. **Actions** exposes other engineering relationships and off-diagram configuration; **Data & interfaces** holds reusable payloads and contracts. -4. Create focused views. **Containment** nests semantic children; **Architecture · allocated work** nests work within performers; **Functional dataflow** shows work decomposition and exchanges; **Missions & capabilities** shows objectives and participation. Dashed, italic parent work denotes an allocation summary inferred from its leaves. Ports appear on their owner's boundary. Placement dragging changes geometry. **Hide** removes a placement; **Delete from model** removes semantics and dependent references. Use **Placement** and **View properties** for exact values. +4. Create focused views. **Containment** nests semantic children; **Architecture · allocated work** nests work within performers; **Functional dataflow** shows work decomposition and exchanges; **Missions & capabilities** shows objectives and participation. Dashed, italic parent work denotes an allocation summary inferred from its leaves. Ports appear on their owner's boundary. **View properties** stages saved line-kind, category, behavior-coverage, context, label and individual-hide choices. Right-click empty diagram space for **Duplicate view**, retaining the same semantic records in an independent presentation. Placement **Hide** removes a placement; line hiding retains its relationship; **Delete from model** removes semantics and dependent references. 5. In **Behavior & scenarios**, author processes/chains, scenario lifelines and ordered occurrences, including repeated uses of the same exchange and explicit fragments/branches. Select a State machine and choose **Create diagram** to place its existing regions and states/modes in a saved view. State machines also support availability and state-action links. Conditions are descriptive, not executable simulation code. Behavior and Data authoring preserve your other diagram layouts. -6. Select an OA or SA element and choose **Create / update counterpart**. Review dependency scope and counterpart choices, then apply one undoable transaction. For an existing trace, **Review semantics…** compares typed content and prior observations, with explicit keep, take or final-value decisions for supported structural, scalar, constructor and individual-row changes. Review does not automatically synchronize the perspectives. Ordered, attachment, artifact and multi-source review controls remain unfinished. Manual traces link other engineering correspondences. **Traceability** also edits artifact URI, revision and fragment metadata. +6. Select an OA or SA element and choose **Create / update counterpart**. Review dependency scope and counterpart choices, then apply one undoable transaction. For an existing trace, **Review semantics…** compares typed content and prior observations, with explicit keep, take or final-value decisions for supported structural, scalar, constructor, individual-row and ordered-collection changes. Open additional trace rows and choose **Include this row** to combine reviewed source contributions into one validated Apply/Undo transaction; conflicting decisions require resolution. Review does not automatically synchronize the perspectives. **Attachments** reviews an existing child’s parent or a relationship endpoint. **Artifact links** reviews metadata, owner moves, explicit source-absence deletion and creation of a reserved counterpart after all introduced metadata is reviewed. These controls have automated coverage; their native acceptance remains pending. Manual traces link other engineering correspondences. **Traceability** also edits artifact URI, revision and fragment metadata. 7. Review **Model checks**, then save `.arcadia` source or `.json`. Warnings allow incomplete authoring; structural errors preserve the committed model and rejected draft. **Model explorer** groups roots by engineering purpose and opens one container's direct children. Breadcrumbs navigate ownership; **Allocated work** is listed separately. Search names, kinds or `@aliases` to find elements with ancestor context. Results are paged in groups of 100. **Collapse all** folds the outline; **Locate** clears the filter and reveals the selected element's ancestry. @@ -69,7 +69,9 @@ The example opens in an architecture view with work nested inside its performers The inspector exposes typed properties, references, ordered lists, aliases and UUID identity. Applying or navigating away attempts to commit the draft; invalid values remain editable. **Escaped strings (advanced)** uses quoted JSON escapes for exact tabs, line breaks and carriage returns, for example `"First\nSecond"`. UUID changes atomically update dependent references. Accepted model/source history retains up to 100 edits within an estimated 32 MiB history budget. -Choose **Interaction**, **Functional exchange** or **Boundary exchange** in the diagram palette, then click the source and a highlighted compatible target, or drag between them. Functional exchanges use output/input ports; function cards create or reuse real ports atomically. Boundary exchanges use compatible component ports. Middle-button drag pans without losing the chosen source; arrow keys also pan while drawing with the canvas focused. Click empty space to restart, or Escape to cancel. Each completed connection is one model/source history operation; edit its name and carried items in Properties. +Choose **Interaction**, **Communication means**, **Functional exchange** or **Boundary exchange** in the diagram palette, then click the source and a highlighted compatible target, or drag between them. Communication means use OA participants directly. Functional exchanges use output/input ports; function cards create or reuse real ports atomically. Boundary exchanges use compatible component ports. Middle-button drag pans without losing the chosen source; arrow keys also pan while drawing with the canvas focused. Click empty space to restart, or Escape to cancel. Each completed connection is one model/source history operation; edit its name and carried items in Properties. + +Create named **Exchange categories** at the perspective root to group Work exchanges or Participant exchanges. In **View properties**, each family defaults to All, including uncategorized exchanges. Selected categories combine by union; an explicit empty selection shows none of that family. Coverage and selected process/chain/scenario contexts intersect with category choices. Explicit hides survive changes to categories and labels. These settings change the saved presentation, not model relationships or node geometry. The [native record](harness/release-current-native-verification.md) covers the source-6 batch crash-recovery/Apply/Undo/Redo flow and a scoped forty-exchange view, including filtering, duplication, hiding and reopening. Communication authoring, remaining view combinations and latest attachment/artifact native acceptance remain pending. In **Select / move**, drag cards on a 10-unit grid, drag empty space to pan, and scroll to zoom. Drag ports along their owner's border or onto any of its four sides. Automatic sides account for direction and visible connections. Manual positions are per diagram and remain relative to the owner through movement or resize; right-click **Reset automatic position** restores automatic placement. **Arrange** changes the current view layout; **Fit** frames its projected bounds. SVG exports the current architecture diagram as a view, not an editable model file. @@ -84,7 +86,7 @@ In the state view, **Select / move** drags vertices and containers without repar **Textual model** has a source-file folder tree, per-file drafts, syntax highlighting, live linting and explicit **Apply all**. Collapse/Locate navigate the hierarchy; collapsed folders retain draft and diagnostic indicators. Right-click a file for contextual actions. Projects can use relative includes to organize source into files. For example: ```text -arcadia 5; +arcadia 6; project harbor "Harbor assistance" { oa_entity control "Harbor control" {} oa_activity respond "Respond to requests" { @@ -118,7 +120,9 @@ Ctrl+S saves to the current local or hosted destination, Ctrl+Shift+S opens Save **Save all** validates a snapshot and saves the source bundle, comparing previously read dependency bytes to reject detected external changes. Each file is replaced atomically; a staged transaction journal allows explicit recovery of interrupted multi-file saves. This is not an atomic multi-file filesystem operation or distributed locking. If recovery is needed, **Recover interrupted save** appears in Textual model. Dirty New/Open/Close operations offer save/discard/cancel. Share or commit the entry file and all included source files together. -While work is unsaved, the desktop attempts a private recovery checkpoint every 10 seconds. It retains the accepted source bundle, raw source buffers, supported pending property/behavior forms and workflow drafts, including rejected input and semantic-review choices. **Home → Recover** opens an unsaved copy with no local or hosted save destination; use **Save as** to choose one. Undo/redo history is not restored. Checkpoints never write model files or save to the server, and credentials are excluded. A failed checkpoint is reported on Home; recent edits since the last completed checkpoint may be unavailable after a crash. Local history and checkpoints live under `%LOCALAPPDATA%/ArcadiaStudio` on Windows or `$XDG_DATA_HOME/arcadia-studio` (otherwise `~/.local/share/arcadia-studio`) on Linux. Explicit saves and version control remain the durable record. Home/recovery is implemented; current native acceptance remains pending in the [coverage audit](harness/release-current-coverage-20260906.md). +While work is unsaved, the desktop attempts a private recovery checkpoint every 10 seconds. It retains the accepted source bundle, raw source buffers, supported pending property/behavior forms and workflow drafts, including rejected input and semantic-review choices. **Home → Recover** opens an unsaved copy with no local or hosted save destination; use **Save as** to choose one. Undo/redo history is not restored. Checkpoints never write model files or save to the server, and credentials are excluded. A failed checkpoint is reported on Home; recent edits since the last completed checkpoint may be unavailable after a crash. Local history and checkpoints live under `%LOCALAPPDATA%/ArcadiaStudio` on Windows or `$XDG_DATA_HOME/arcadia-studio` (otherwise `~/.local/share/arcadia-studio`) on Linux. Explicit saves and version control remain the durable record. + +The [current Windows native record](harness/release-current-native-verification.md) verifies scoped Home/recents workflows and forced-stop recovery of raw invalid source, invalid UUID properties, a pending timed transition, keyed semantic choices and a retained rejected order draft. Accepted repairs were saved and compared with independent source oracles, including exact Undo/Redo cases. It also records the older binary's transient journal-publication failure; the bounded fix has automated evidence in the [journal review](harness/release-journal-publication-review-20260906.md) and awaits new-binary native verification. Subsequent source-6 batch recovery and exact Apply/Save/Undo/Redo comparisons are recorded there too. These results do not accept all recovery variants, latest attachment/artifact workflows, Linux or the complete release. ## REST service and external tools @@ -149,6 +153,8 @@ To preserve authored files and comments, use `POST /api/v1/projects/import-sourc ## Development +The 2026-09-06 source-6 verification accounts for **943 passing tests and one existing ignored benchmark** across the full workspace run and complete affected-suite reruns after three compatibility-expectation corrections. The **48 HTTP tests** are included in that coverage, not additional to it. Source-6 OpenAPI validation passes 39 fixtures and checks 197 component schemas. [Current state](harness/STATE.md) identifies the exact logs and outstanding checks; this evidence does not claim one uninterrupted green workspace run or accept subsequent edits. Fresh GUI/server builds, strict checks for the final tree, source-6 native workflows, Linux/MSRV verification and current release packages remain release gates. + ```sh cargo fmt --all -- --check cargo clippy --workspace --all-targets --locked -- -D warnings diff --git a/crates/oa-core/Cargo.toml b/crates/oa-core/Cargo.toml index 0ab48cda..df5452b5 100644 --- a/crates/oa-core/Cargo.toml +++ b/crates/oa-core/Cargo.toml @@ -12,3 +12,6 @@ uuid.workspace = true thiserror.workspace = true tempfile = "3" same-file = "1" + +[target.'cfg(target_os = "linux")'.dependencies] +libc = "0.2" diff --git a/crates/oa-core/examples/generate_release1.rs b/crates/oa-core/examples/generate_release1.rs new file mode 100644 index 00000000..4f2e9451 --- /dev/null +++ b/crates/oa-core/examples/generate_release1.rs @@ -0,0 +1,53 @@ +//! Generate only the new teaching-project files; old compatibility examples stay untouched. +#[path = "../../../examples/release1/model.rs"] +mod teaching; + +fn main() -> Result<(), Box> { + let directory = std::env::args_os() + .nth(1) + .map(std::path::PathBuf::from) + .unwrap_or_else(|| "examples/release1".into()); + let lesson = teaching::build(); + let bundle = teaching::checked_bundle(&lesson.project); + for (name, source) in &bundle.files { + let path = directory.join(name); + std::fs::create_dir_all(path.parent().unwrap())?; + std::fs::write(path, source)?; + } + for (name, value) in [ + ("model.json", serde_json::to_value(&lesson.project)?), + ( + "transition-before.json", + serde_json::to_value(&lesson.before_transition)?, + ), + ( + "transition-request.json", + serde_json::to_value(&lesson.request)?, + ), + ( + "expected-diagnostics.json", + serde_json::to_value(oa_core::validation::validate(&lesson.project))?, + ), + ] { + std::fs::write( + directory.join(name), + serde_json::to_string_pretty(&value)? + "\n", + )?; + } + println!( + "Validated {} elements, {} relations, {} views, {} traces; wrote {} source files to {}", + lesson.project.elements.len(), + lesson.project.relations.len(), + lesson.project.diagrams.len(), + lesson.project.traces.len(), + bundle.files.len(), + directory.display() + ); + for issue in oa_core::validation::validate(&lesson.project) { + println!( + "{:?} {} {:?}: {}", + issue.severity, issue.code, issue.element, issue.message + ); + } + Ok(()) +} diff --git a/crates/oa-core/examples/release_dense_views.rs b/crates/oa-core/examples/release_dense_views.rs new file mode 100644 index 00000000..fe226389 --- /dev/null +++ b/crates/oa-core/examples/release_dense_views.rs @@ -0,0 +1,441 @@ +//! Deterministic dense-view fixture and semantic oracle. No GUI or authored routing. +//! cargo run -p oa-core --example release_dense_views -- OUTPUT_DIRECTORY +use oa_core::{view_options::*, *}; +use serde_json::json; +use std::{ + collections::{BTreeMap, BTreeSet}, + error::Error, + fs, + path::PathBuf, + time::Instant, +}; + +fn id(n: u32) -> Id { + format!("d4060906-0000-4000-8000-{n:012x}").parse().unwrap() +} + +fn element(p: &mut Project, n: u32, kind: Kind, alias: &str, name: &str, parent: Option) -> Id { + let identity = id(n); + assert!( + p.elements + .insert( + identity, + Element { + id: identity, + alias: alias.into(), + name: name.into(), + description: String::new(), + parent, + body: ElementBody::new(kind, Layer::Oa).unwrap() + } + ) + .is_none() + ); + identity +} + +fn relation(p: &mut Project, n: u32, alias: &str, name: &str, body: RelationBody) -> Id { + let identity = id(n); + assert!( + p.relations + .insert( + identity, + Relation { + id: identity, + alias: alias.into(), + name: name.into(), + description: String::new(), + body + } + ) + .is_none() + ); + identity +} + +fn fixture() -> (Project, Id, Vec, Vec, Vec) { + let mut p = Project::empty(); + p.id = id(1); + p.alias = "dense_service_views".into(); + p.name = "Forty service exchanges".into(); + p.description = "Deterministic release fixture. Forty actual work exchanges share two endpoints; no authored routes or synthetic endpoints.".into(); + let requester = element(&mut p, 10, Kind::Entity, "requester", "Requester", None); + let provider = element(&mut p, 11, Kind::Entity, "provider", "Provider", None); + let a = element( + &mut p, + 20, + Kind::Activity, + "request_service", + "Request service", + None, + ); + let b = element( + &mut p, + 21, + Kind::Activity, + "provide_service", + "Provide service", + None, + ); + relation( + &mut p, + 3000, + "requester_work", + "Requester responsibility", + RelationBody::Allocation { + source: requester, + target: a, + }, + ); + relation( + &mut p, + 3001, + "provider_work", + "Provider responsibility", + RelationBody::Allocation { + source: provider, + target: b, + }, + ); + let group_names = ["Commands", "Status", "Alerts", "Measurements"]; + let items: Vec<_> = group_names + .iter() + .enumerate() + .map(|(i, name)| { + element( + &mut p, + 100 + i as u32, + Kind::ExchangeItem, + &format!("payload_{}", i + 1), + &format!("{name} payload"), + None, + ) + }) + .collect(); + let exchanges: Vec<_> = (0..40) + .map(|i| { + relation( + &mut p, + 2000 + i, + &format!("service_data_{:02}", i + 1), + &format!("Service data {:02}", i + 1), + RelationBody::Interaction { + source: a, + target: b, + items: vec![items[(i / 10) as usize]], + }, + ) + }) + .collect(); + let mut categories = vec![]; + for (i, name) in group_names.iter().enumerate() { + let category = element( + &mut p, + 200 + i as u32, + Kind::ExchangeCategory, + &format!("concern_{}", i + 1), + name, + None, + ); + *p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = + SharedBody::ExchangeCategory { + family: ExchangeFamily::Work, + exchanges: exchanges[i * 10..i * 10 + 10].to_vec(), + }; + categories.push(category); + } + let overlapping = element( + &mut p, + 204, + Kind::ExchangeCategory, + "critical_concern", + "Critical cross-cutting exchanges", + None, + ); + *p.elements + .get_mut(&overlapping) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::ExchangeCategory { + family: ExchangeFamily::Work, + exchanges: [0, 10, 20, 30].map(|i| exchanges[i]).to_vec(), + }; + categories.push(overlapping); + let mut contexts = vec![]; + for (group, name) in group_names[..3].iter().enumerate() { + let process = element( + &mut p, + 300 + group as u32, + Kind::Process, + &format!("process_{}", group + 1), + &format!("{name} process"), + None, + ); + for offset in 0..10 { + let index = group * 10 + offset; + let occurrence = element( + &mut p, + 1000 + index as u32, + Kind::Occurrence, + &format!("use_data_{:02}", index + 1), + &format!("Use service data {:02}", index + 1), + Some(process), + ); + *p.elements + .get_mut(&occurrence) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Exchange { + exchange: Some(exchanges[index]), + sender: None, + receiver: None, + message_kind: MessageKind::Asynchronous, + reply_to: None, + values: vec![], + }, + precondition: String::new(), + postcondition: String::new(), + }; + p.elements + .get_mut(&process) + .unwrap() + .steps_mut() + .unwrap() + .push(occurrence); + } + contexts.push(process); + } + let diagram = id(4000); + let nodes = [(a, 80.), (b, 760.)] + .into_iter() + .enumerate() + .map(|(i, (element, x))| { + let id = id(4100 + i as u32); + ( + id, + NodePlacement { + id, + alias: format!("service_placement_{}", i + 1), + element, + position: Position { x, y: 160. }, + size: Size { + width: 230., + height: 150., + }, + collapsed: false, + port_anchors: vec![], + }, + ) + }) + .collect(); + p.diagrams.insert(diagram, Diagram { id: diagram, alias: "forty_exchange_view".into(), name: "Forty service exchanges · all concerns".into(), description: "Start with All. Compare first category, overlapping concerns, global coverage and chosen processes. No hand-routed edges.".into(), layer: Layer::Oa, projection: ProjectionKind::FunctionalDataflow, view_options: ViewOptions::default(), nodes, routes: vec![] }); + (p, diagram, exchanges, categories, contexts) +} + +fn projected(p: &Project, diagram: &Diagram) -> BTreeSet { + projection::project_diagram(p, diagram) + .edges + .into_iter() + .map(|edge| edge.id) + .collect() +} + +fn overlap(a: routing::RoutingBounds, b: routing::RoutingBounds) -> bool { + a.position.x < b.position.x + b.size.width + && b.position.x < a.position.x + a.size.width + && a.position.y < b.position.y + b.size.height + && b.position.y < a.position.y + a.size.height +} + +fn main() -> Result<(), Box> { + let output = PathBuf::from( + std::env::args_os() + .nth(1) + .ok_or("Supply an output directory for the dense-view fixture.")?, + ); + let (p, diagram, exchanges, categories, contexts) = fixture(); + let issues = validation::validate(&p); + assert!( + !issues + .iter() + .any(|issue| issue.severity == validation::Severity::Error), + "{issues:?}" + ); + let pristine = p.clone(); + let original = &p.diagrams[&diagram]; + assert!(original.routes.is_empty()); + let all = projection::project_view(&p, diagram); + assert_eq!(all.nodes.len(), 2); + assert_eq!(all.edges.len(), 40); + assert_eq!( + all.edges + .iter() + .map(|edge| (edge.source, edge.target)) + .collect::>() + .len(), + 1 + ); + let sets = |ids: &[Id]| ids.iter().copied().collect::>(); + let cases = vec![ + ("all", ViewOptions::default(), sets(&exchanges)), + ( + "first_category", + ViewOptions { + work_categories: CategorySelection::Any(vec![categories[0]]), + ..ViewOptions::default() + }, + sets(&exchanges[..10]), + ), + ( + "first_and_overlap", + ViewOptions { + work_categories: CategorySelection::Any(vec![categories[0], categories[4]]), + ..ViewOptions::default() + }, + exchanges[..10] + .iter() + .copied() + .chain([exchanges[10], exchanges[20], exchanges[30]]) + .collect(), + ), + ( + "in_behavior", + ViewOptions { + coverage: Coverage::InBehavior, + ..ViewOptions::default() + }, + sets(&exchanges[..30]), + ), + ( + "outside_behavior", + ViewOptions { + coverage: Coverage::OutsideBehavior, + ..ViewOptions::default() + }, + sets(&exchanges[30..]), + ), + ( + "second_process", + ViewOptions { + work_contexts: ContextSelection::Any(vec![contexts[1]]), + ..ViewOptions::default() + }, + sets(&exchanges[10..20]), + ), + ( + "outside_second_process", + ViewOptions { + coverage: Coverage::OutsideBehavior, + work_contexts: ContextSelection::Any(vec![contexts[1]]), + ..ViewOptions::default() + }, + BTreeSet::new(), + ), + ( + "empty_categories", + ViewOptions { + work_categories: CategorySelection::Any(vec![]), + ..ViewOptions::default() + }, + BTreeSet::new(), + ), + ( + "empty_contexts", + ViewOptions { + work_contexts: ContextSelection::Any(vec![]), + ..ViewOptions::default() + }, + BTreeSet::new(), + ), + ( + "no_lines", + ViewOptions { + relation_kinds: RelationKinds::Only(vec![]), + ..ViewOptions::default() + }, + BTreeSet::new(), + ), + ( + "first_hide_05", + ViewOptions { + hidden_edges: vec![exchanges[4]], + work_categories: CategorySelection::Any(vec![categories[0]]), + exchange_labels: ExchangeLabels::NameAndItems, + ..ViewOptions::default() + }, + exchanges[..10] + .iter() + .filter(|edge| **edge != exchanges[4]) + .copied() + .collect(), + ), + ]; + let mut case_results = BTreeMap::new(); + for (name, options, expected) in cases { + assert!(validate_options(&p, Layer::Oa, &options).is_empty()); + let mut changed = original.clone(); + changed.view_options = options; + assert_eq!(projected(&p, &changed), expected, "{name}"); + let view = projection::project_diagram(&p, &changed); + assert_eq!( + view.nodes, all.nodes, + "{name}: visibility must not move nodes" + ); + assert_eq!( + view.ports, all.ports, + "{name}: visibility must not move ports" + ); + assert!(changed.routes.is_empty()); + case_results.insert(name, json!({"count":expected.len(),"edges":expected})); + } + assert_eq!( + ExchangeLabels::Items.label(&p, exchanges[0]), + "Commands payload" + ); + assert_eq!( + ExchangeLabels::NameAndItems.label(&p, exchanges[39]), + "Service data 40 · Measurements payload" + ); + let began = Instant::now(); + let routed = routing::route_diagram(&p, original, &all); + let route_ms = began.elapsed().as_millis(); + assert_eq!(routed.edges.len(), 40); + let edges: Vec<_> = routed.edges.values().collect(); + let mut label_overlaps = vec![]; + let mut coincident_paths = vec![]; + for (i, first) in edges.iter().enumerate() { + for second in &edges[i + 1..] { + if overlap(first.label.bounds, second.label.bounds) { + label_overlaps.push([first.id, second.id]); + } + if first.points == second.points { + coincident_paths.push([first.id, second.id]); + } + } + } + let source = language::format(&p); + assert_eq!( + language::parse(&source).expect("Canonical fixture source must reopen"), + p + ); + let native_json = serde_json::to_vec_pretty(&p)?; + assert_eq!(persistence::decode(&native_json)?, p); + assert_eq!( + p, pristine, + "Oracles and routing must not mutate model facts" + ); + let report = json!({"schema":p.schema_version,"project":p.id,"diagram":diagram,"semantic_oracles":"passed","elements":p.elements.len(),"relations":p.relations.len(),"cases":case_results, + "routing_ms":route_ms,"label_overlap_pairs":label_overlaps,"exactly_coincident_path_pairs":coincident_paths, + "routing_warnings":edges.iter().map(|edge| json!({"edge":edge.id,"warnings":edge.warnings.iter().map(ToString::to_string).collect::>()})).collect::>(), + "native_readability":"pending; metrics do not establish a usability pass"}); + fs::create_dir_all(&output)?; + fs::write(output.join("dense-40.arcadia"), source)?; + fs::write(output.join("dense-40.json"), native_json)?; + fs::write( + output.join("dense-40-oracle.json"), + serde_json::to_vec_pretty(&report)?, + )?; + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} diff --git a/crates/oa-core/src/demo.rs b/crates/oa-core/src/demo.rs index 092500e6..1d3ab09a 100644 --- a/crates/oa-core/src/demo.rs +++ b/crates/oa-core/src/demo.rs @@ -78,6 +78,7 @@ fn diagram( description: "A focused projection of the shared engineering model.".into(), layer, projection, + view_options: Default::default(), nodes: BTreeMap::new(), routes: vec![], }, diff --git a/crates/oa-core/src/language.rs b/crates/oa-core/src/language.rs index 231823b6..dc6aee3b 100644 --- a/crates/oa-core/src/language.rs +++ b/crates/oa-core/src/language.rs @@ -350,6 +350,9 @@ fn reference_field(key: &str) -> bool { | "lifeline" | "state" | "constrained" + | "exchanges" + | "hidden_edges" + | "Any" | "source" | "target" | "source_port" @@ -1383,9 +1386,9 @@ impl Parser<'_> { fn unit(&mut self, entry: bool, includes_allowed: bool) -> Result { let mut header = if entry { self.take("arcadia")?; - if !matches!(self.text(), "3" | "4" | "5") { + if !matches!(self.text(), "3" | "4" | "5" | "6") { return Err(self - .error("Unsupported schema version: expected 'arcadia 3;', 'arcadia 4;' or 'arcadia 5;'.")); + .error("Unsupported schema version: expected 'arcadia 3;', 'arcadia 4;', 'arcadia 5;' or 'arcadia 6;'.")); } let schema_version = self.text().parse().expect("checked source version"); self.cursor += 1; @@ -1603,6 +1606,9 @@ pub(crate) fn resolve_declarations( }; match d.kind { DeclKind::Element(kind, layer) => { + if project.schema_version < 6 && !crate::persistence::pre6_element_kind(kind) { + return Err(conversion_error(std::format!("{kind:?} requires schema/source 6."))); + } if legacy && !crate::persistence::v3::element_allowed(serde_json::to_value(kind).unwrap().as_str().unwrap()) { return Err(conversion_error("Element constructor was not part of schema3.".into())); } @@ -1677,6 +1683,9 @@ pub(crate) fn resolve_declarations( ); } DeclKind::Relation(kind) => { + if project.schema_version < 6 && !crate::persistence::pre6_relation_kind(kind) { + return Err(conversion_error(std::format!("{kind:?} requires schema/source 6."))); + } if legacy && !crate::persistence::v3::relation_allowed(serde_json::to_value(kind).unwrap().as_str().unwrap()) { return Err(conversion_error("Relation constructor was not part of schema3.".into())); } @@ -1713,8 +1722,14 @@ pub(crate) fn resolve_declarations( ); } DeclKind::Diagram => { + if project.schema_version < 6 && fields.contains_key("view_options") { + return Err(conversion_error("Diagram.view_options requires schema/source 6, including an explicitly supplied default value.".into())); + } + if project.schema_version < 6 && let Some(key)=fields.keys().find(|key| !matches!(key.as_str(),"description"|"layer"|"projection"|"routes")) { + return Err(conversion_error(std::format!("Unknown diagram field '{key}' in the frozen pre6 contract."))); + } let defaults = - serde_json::json!({"description":"","layer":"Oa","projection":"Overview","routes":[]}); + serde_json::json!({"description":"","layer":"Oa","projection":"Overview","routes":[],"view_options":crate::view_options::ViewOptions::default()}); let mut record = merge_fields(defaults.as_object().unwrap().clone(), fields) .map_err(&conversion_error)?; header(&mut record, d, true); @@ -1837,8 +1852,13 @@ pub(crate) fn resolve_declarations( } return Ok(migrated); } - if project.schema_version == 4 { - return crate::persistence::v4::finish(project, limits).map_err(|error| { + if project.schema_version == 4 || project.schema_version == 5 { + return (if project.schema_version == 4 { + crate::persistence::v4::finish(project, limits) + } else { + crate::persistence::migrate_v5(project, limits) + }) + .map_err(|error| { ( usize::MAX, Error { diff --git a/crates/oa-core/src/language/semantic_source.rs b/crates/oa-core/src/language/semantic_source.rs index b6382b91..fa183835 100644 --- a/crates/oa-core/src/language/semantic_source.rs +++ b/crates/oa-core/src/language/semantic_source.rs @@ -111,6 +111,10 @@ pub(super) fn resolve_baseline( let json = raw(value)?; let baseline: SemanticBaseline = serde_json::from_value(json.clone()) .map_err(|cause| error(value, format!("Invalid semantic baseline: {cause}")))?; + if version < 6 { + crate::persistence::check_pre6_baseline(&baseline) + .map_err(|cause| error(value, cause.to_string()))?; + } let slots = historical_source_slots(&baseline).into_iter().collect(); validate_tokens(value, &mut vec![], &slots)?; resolved.insert(key.clone(), json); diff --git a/crates/oa-core/src/lib.rs b/crates/oa-core/src/lib.rs index e8aadf25..72339176 100644 --- a/crates/oa-core/src/lib.rs +++ b/crates/oa-core/src/lib.rs @@ -13,6 +13,7 @@ pub mod source_files; pub mod source_project; pub mod transitions; pub mod validation; +pub mod view_options; pub use model::*; pub use ports::*; pub use semantic::*; diff --git a/crates/oa-core/src/migration_v4.rs b/crates/oa-core/src/migration_v4.rs index 98923eeb..6127f724 100644 --- a/crates/oa-core/src/migration_v4.rs +++ b/crates/oa-core/src/migration_v4.rs @@ -67,6 +67,8 @@ impl Project4 { if self.schema_version != 4 { return Err(FileError::Invalid("Expected schema_version 4.".into())); } + crate::persistence::check_pre6_relations(self.relations.values())?; + crate::persistence::check_pre6_elements(self.elements.values())?; Ok(Project { schema_version: SCHEMA_VERSION, id: self.id, @@ -102,6 +104,8 @@ pub(crate) fn review_defaults(project: &Project, report: &mut MigrationReport) { } pub(crate) fn finish(mut project: Project, limits: ParseLimits) -> Result { + crate::persistence::check_pre6_relations(project.relations.values())?; + crate::persistence::check_pre6_elements(project.elements.values())?; project.schema_version = SCHEMA_VERSION; limits.check_model(&project).map_err(FileError::Limit)?; let mut report = MigrationReport::data_upgrade(4); diff --git a/crates/oa-core/src/model.rs b/crates/oa-core/src/model.rs index 868a3b46..490f779c 100644 --- a/crates/oa-core/src/model.rs +++ b/crates/oa-core/src/model.rs @@ -6,7 +6,7 @@ use std::{ use uuid::Uuid; pub type Id = Uuid; -pub const SCHEMA_VERSION: u32 = 5; +pub const SCHEMA_VERSION: u32 = 6; pub const MAX_COORDINATE: f32 = 1_000_000.; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] @@ -39,6 +39,7 @@ pub enum Kind { Mission, FunctionalChain, ExchangeItem, + ExchangeCategory, DataType, DataField, Interface, @@ -65,7 +66,7 @@ pub enum Kind { Constraint, } impl Kind { - pub const ALL: [Self; 38] = [ + pub const ALL: [Self; 39] = [ Self::Entity, Self::Actor, Self::Role, @@ -80,6 +81,7 @@ impl Kind { Self::Mission, Self::FunctionalChain, Self::ExchangeItem, + Self::ExchangeCategory, Self::DataType, Self::DataField, Self::Interface, @@ -121,6 +123,7 @@ impl Kind { Self::Mission => "Mission", Self::FunctionalChain => "Functional chain", Self::ExchangeItem => "Exchange item", + Self::ExchangeCategory => "Exchange category", Self::DataType => "Data type", Self::DataField => "Data field", Self::Interface => "Interface", @@ -564,6 +567,10 @@ pub enum OccurrenceBody { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub enum SharedBody { + ExchangeCategory { + family: crate::view_options::ExchangeFamily, + exchanges: Vec, + }, ExchangeItem { kind: ExchangeItemKind, mechanism: ExchangeMechanism, @@ -754,6 +761,10 @@ impl ElementBody { default: None, unit: String::new(), }, + Kind::ExchangeCategory => SharedBody::ExchangeCategory { + family: crate::view_options::ExchangeFamily::Work, + exchanges: vec![], + }, Kind::DataField => SharedBody::DataField { type_ref: TypeRef::String, minimum: 1, @@ -815,6 +826,7 @@ impl SharedBody { pub fn kind(&self) -> Kind { match self { Self::ExchangeItem { .. } => Kind::ExchangeItem, + Self::ExchangeCategory { .. } => Kind::ExchangeCategory, Self::DataType { .. } => Kind::DataType, Self::DataField { .. } => Kind::DataField, Self::Interface { .. } => Kind::Interface, @@ -844,6 +856,7 @@ pub enum RelationKind { RoleAssignment, FunctionalExchange, BoundaryExchange, + CommunicationMean, MissionCapability, CapabilityInclude, CapabilityExtend, @@ -858,7 +871,7 @@ pub enum RelationKind { StateAction, } impl RelationKind { - pub const ALL: [Self; 23] = [ + pub const ALL: [Self; 24] = [ Self::InterfaceItemAllocation, Self::DataAssociation, Self::Allocation, @@ -870,6 +883,7 @@ impl RelationKind { Self::RoleAssignment, Self::FunctionalExchange, Self::BoundaryExchange, + Self::CommunicationMean, Self::MissionCapability, Self::CapabilityInclude, Self::CapabilityExtend, @@ -902,6 +916,10 @@ impl RelationKind { Self::RoleAssignment => from == Kind::Role && to == Kind::Activity, Self::FunctionalExchange => from == Kind::Function && to == Kind::Function, Self::BoundaryExchange => from == Kind::Port && to == Kind::Port, + Self::CommunicationMean => { + matches!(from, Kind::Entity | Kind::Actor) + && matches!(to, Kind::Entity | Kind::Actor) + } Self::MissionCapability => matches!( (from, to), (Kind::OperationalMission, Kind::Capability) @@ -953,6 +971,7 @@ impl RelationKind { Self::RoleAssignment => "OA role → activity", Self::FunctionalExchange => "SA function → SA function", Self::BoundaryExchange => "Output port → input port", + Self::CommunicationMean => "OA entity/actor → OA entity/actor", Self::MissionCapability => "Mission → capability in the same perspective", Self::CapabilityInclude => "Including capability → included capability", Self::CapabilityExtend => { @@ -963,7 +982,7 @@ impl RelationKind { Self::SequenceLink => "Preceding → following contribution in the same process or chain", Self::StateTransition => "State → state in one machine", Self::ExchangeAllocation => { - "Boundary exchange → operational interaction / functional exchange" + "Communication means / boundary exchange → operational interaction / functional exchange" } Self::InterfaceProvision => "Participant/port → provided interface", Self::InterfaceRequirement => "Participant/port → required interface", @@ -986,6 +1005,7 @@ impl fmt::Display for RelationKind { Self::RoleAssignment => "Role assignment", Self::FunctionalExchange => "Functional exchange", Self::BoundaryExchange => "Boundary exchange", + Self::CommunicationMean => "Communication means", Self::MissionCapability => "Mission capability", Self::CapabilityInclude => "Capability include", Self::CapabilityExtend => "Capability extend", @@ -1074,6 +1094,12 @@ pub enum RelationBody { oriented: bool, items: Vec, }, + CommunicationMean { + source: Id, + target: Id, + oriented: bool, + items: Vec, + }, MissionCapability { source: Id, target: Id, @@ -1160,6 +1186,7 @@ impl RelationBody { Self::RoleAssignment { .. } => RelationKind::RoleAssignment, Self::FunctionalExchange { .. } => RelationKind::FunctionalExchange, Self::BoundaryExchange { .. } => RelationKind::BoundaryExchange, + Self::CommunicationMean { .. } => RelationKind::CommunicationMean, Self::MissionCapability { .. } => RelationKind::MissionCapability, Self::CapabilityInclude { .. } => RelationKind::CapabilityInclude, Self::CapabilityExtend { .. } => RelationKind::CapabilityExtend, @@ -1187,6 +1214,7 @@ impl RelationBody { | Self::RoleAssignment { source, target } | Self::FunctionalExchange { source, target, .. } | Self::BoundaryExchange { source, target, .. } + | Self::CommunicationMean { source, target, .. } | Self::MissionCapability { source, target } | Self::CapabilityInclude { source, target } | Self::CapabilityExtend { source, target, .. } @@ -1214,6 +1242,7 @@ impl RelationBody { | Self::RoleAssignment { source, target } | Self::FunctionalExchange { source, target, .. } | Self::BoundaryExchange { source, target, .. } + | Self::CommunicationMean { source, target, .. } | Self::MissionCapability { source, target } | Self::CapabilityInclude { source, target } | Self::CapabilityExtend { source, target, .. } @@ -1235,7 +1264,8 @@ impl RelationBody { match self { Self::Interaction { items, .. } | Self::FunctionalExchange { items, .. } - | Self::BoundaryExchange { items, .. } => items, + | Self::BoundaryExchange { items, .. } + | Self::CommunicationMean { items, .. } => items, _ => &[], } } @@ -1285,6 +1315,12 @@ impl RelationBody { oriented: true, items: vec![], }, + RelationKind::CommunicationMean => Self::CommunicationMean { + source, + target, + oriented: false, + items: vec![], + }, RelationKind::MissionCapability => Self::MissionCapability { source, target }, RelationKind::CapabilityInclude => Self::CapabilityInclude { source, target }, RelationKind::CapabilityExtend => Self::CapabilityExtend { @@ -1387,6 +1423,8 @@ pub struct Diagram { pub description: String, pub layer: Layer, pub projection: ProjectionKind, + #[serde(default)] + pub view_options: crate::view_options::ViewOptions, #[serde(deserialize_with = "unique_id_map")] pub nodes: BTreeMap, #[serde(default)] @@ -1814,6 +1852,7 @@ impl Project { description: String::new(), layer, projection: ProjectionKind::Overview, + view_options: crate::view_options::ViewOptions::default(), nodes: BTreeMap::new(), routes: vec![], }, diff --git a/crates/oa-core/src/persistence.rs b/crates/oa-core/src/persistence.rs index da2b40c4..f7bc1ded 100644 --- a/crates/oa-core/src/persistence.rs +++ b/crates/oa-core/src/persistence.rs @@ -139,6 +139,36 @@ pub fn decode_with_report_limited( } let decoded = if bytes.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'{') { let header: serde_json::Value = serde_json::from_slice(bytes)?; + if header + .get("schema_version") + .and_then(serde_json::Value::as_u64) + .is_some_and(|v| v <= 5) + && let Some(diagrams) = header + .get("diagrams") + .and_then(serde_json::Value::as_object) + { + for diagram in diagrams.values().filter_map(serde_json::Value::as_object) { + for key in diagram.keys() { + if key == "view_options" { + return Err(FileError::Invalid("Diagram.view_options requires schema/source 6, including an explicitly supplied default value.".into())); + } + if !matches!( + key.as_str(), + "id" | "alias" + | "name" + | "description" + | "layer" + | "projection" + | "nodes" + | "routes" + ) { + return Err(FileError::Invalid(format!( + "Unknown diagram field '{key}' in the frozen pre6 contract." + ))); + } + } + } + } match header .get("schema_version") .and_then(serde_json::Value::as_u64) @@ -151,13 +181,14 @@ pub fn decode_with_report_limited( Some(2) => migrate_v2_limited(serde_json::from_slice(bytes)?, limits)?, Some(3) => v3::migrate(serde_json::from_slice(bytes)?, limits)?, Some(4) => v4::migrate(serde_json::from_slice(bytes)?, limits)?, - Some(5) => DecodeResult { + Some(5) => migrate_v5(serde_json::from_slice(bytes)?, limits)?, + Some(6) => DecodeResult { project: serde_json::from_slice(bytes)?, migration: None, }, _ => { return Err(FileError::Invalid( - "Unsupported or missing schema_version; expected 1, 2, 3, 4 or 5.".into(), + "Unsupported or missing schema_version; expected 1, 2, 3, 4, 5 or 6.".into(), )); } } @@ -186,6 +217,146 @@ pub fn decode_with_report_limited( Ok(decoded) } +/// Versions 4 and 5 share unchanged typed records, but their constructor set +/// remains frozen. Apply this before normalizing the advertised version. +pub(crate) fn pre6_relation_kind(kind: RelationKind) -> bool { + matches!( + kind, + RelationKind::InterfaceItemAllocation + | RelationKind::DataAssociation + | RelationKind::Allocation + | RelationKind::PortAllocation + | RelationKind::Interaction + | RelationKind::CapabilityInvolvement + | RelationKind::ChainInvolvement + | RelationKind::RoleRealization + | RelationKind::RoleAssignment + | RelationKind::FunctionalExchange + | RelationKind::BoundaryExchange + | RelationKind::MissionCapability + | RelationKind::CapabilityInclude + | RelationKind::CapabilityExtend + | RelationKind::CapabilityGeneralization + | RelationKind::MissionInvolvement + | RelationKind::SequenceLink + | RelationKind::StateTransition + | RelationKind::Availability + | RelationKind::ExchangeAllocation + | RelationKind::InterfaceProvision + | RelationKind::InterfaceRequirement + | RelationKind::StateAction + ) +} +pub(crate) fn pre6_element_kind(kind: Kind) -> bool { + matches!( + kind, + Kind::Entity + | Kind::Actor + | Kind::Role + | Kind::Activity + | Kind::Capability + | Kind::OperationalMission + | Kind::Process + | Kind::System + | Kind::SystemActor + | Kind::Function + | Kind::SystemCapability + | Kind::Mission + | Kind::FunctionalChain + | Kind::ExchangeItem + | Kind::DataType + | Kind::DataField + | Kind::Interface + | Kind::Port + | Kind::Scenario + | Kind::Lifeline + | Kind::Occurrence + | Kind::StateMachine + | Kind::Region + | Kind::State + | Kind::Mode + | Kind::Initial + | Kind::Final + | Kind::Choice + | Kind::Junction + | Kind::Fork + | Kind::Join + | Kind::ShallowHistory + | Kind::DeepHistory + | Kind::EntryPoint + | Kind::ExitPoint + | Kind::Terminate + | Kind::Event + | Kind::Constraint + ) +} +pub(crate) fn check_pre6_relations<'a>( + relations: impl Iterator, +) -> Result<(), FileError> { + for relation in relations { + if !pre6_relation_kind(relation.kind()) { + return Err(FileError::Invalid(format!( + "{:?} requires schema/source 6.", + relation.kind() + ))); + } + } + Ok(()) +} +pub(crate) fn check_pre6_elements<'a>( + elements: impl Iterator, +) -> Result<(), FileError> { + for element in elements { + if !pre6_element_kind(element.kind()) { + return Err(FileError::Invalid(format!( + "{:?} requires schema/source 6.", + element.kind() + ))); + } + } + Ok(()) +} +pub(crate) fn check_pre6_baseline(baseline: &SemanticBaseline) -> Result<(), FileError> { + if baseline.facets.iter().any(|f| { + matches!( + f, + FacetReview::Category(_) + | FacetReview::Member { + collection: ReferenceCollection::CategoryExchanges, + .. + } + ) + }) { + return Err(FileError::Invalid( + "Exchange category semantic facts require schema/source 6.".into(), + )); + } + Ok(()) +} +pub(crate) fn migrate_v5( + mut project: Project, + limits: crate::language::ParseLimits, +) -> Result { + if project.schema_version != 5 { + return Err(FileError::Invalid("Expected schema_version 5.".into())); + } + check_pre6_relations(project.relations.values())?; + check_pre6_elements(project.elements.values())?; + for baseline in project + .traces + .values() + .filter_map(|t| t.baseline.as_ref().and_then(|b| b.semantic.as_ref())) + { + check_pre6_baseline(baseline)?; + } + limits.check_model(&project).map_err(FileError::Limit)?; + project.schema_version = SCHEMA_VERSION; + Ok(DecodeResult { + project, + migration: Some(MigrationReport::data_upgrade(5)), + }) +} + pub(crate) fn migrate_v1_with_report_limited( old: v1::Project, limits: crate::language::ParseLimits, diff --git a/crates/oa-core/src/ports.rs b/crates/oa-core/src/ports.rs index 90c06833..2c5b09a9 100644 --- a/crates/oa-core/src/ports.rs +++ b/crates/oa-core/src/ports.rs @@ -1182,6 +1182,48 @@ pub(crate) fn validate_exchange_allocation(p: &Project, r: &Relation, out: &mut else { return; }; + if let RelationBody::CommunicationMean { + source, + target, + oriented, + .. + } = &boundary.body + { + let actual = ( + actual_performer(p, work.source()), + actual_performer(p, work.target()), + ); + let compatible = |a, b| { + actual.0.is_none_or(|owner| owner == a) && actual.1.is_none_or(|owner| owner == b) + }; + if !compatible(*source, *target) && (*oriented || !compatible(*target, *source)) { + error( + out, + r.id, + "exchange_allocation_performer", + "endpoints", + "Actual activity allocations must agree with one permitted communication direction; review the means or the activity allocations.", + ); + } else if actual.0.is_none() || actual.1.is_none() { + warning( + out, + r.id, + "incomplete_exchange_allocation", + "endpoints", + "Add or review actual activity-to-participant Allocations; role responsibility alone does not resolve communication allocation.", + ); + } + if !items_satisfy(p, work.body.items(), boundary.body.items()) { + error( + out, + r.id, + "exchange_allocation_items", + "items", + "Communication means must declare every allocated interaction item; review the carried item contract explicitly.", + ); + } + return; + } let RelationBody::BoundaryExchange { source: a, target: b, @@ -1219,7 +1261,11 @@ pub(crate) fn validate_exchange_allocation(p: &Project, r: &Relation, out: &mut r.id, "legacy_exchange_allocation", "endpoints", - "Resolve legacy ports and explicitly allocate the corresponding typed ports.", + if p.layer_of(boundary.id) == Some(Layer::Oa) { + "This preserved OA boundary uses legacy ports; review its communication representation without inventing SA port contracts." + } else { + "Resolve legacy ports and explicitly allocate the corresponding typed ports." + }, ); for (function, port) in [(work.source(), *a), (work.target(), *b)] { match (actual_performer(p, function), owner(port)) { diff --git a/crates/oa-core/src/projection.rs b/crates/oa-core/src/projection.rs index 9e06706b..befb027b 100644 --- a/crates/oa-core/src/projection.rs +++ b/crates/oa-core/src/projection.rs @@ -226,8 +226,58 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView { }; project_diagram(p, d) } +/// Reuse an index built for this accepted semantic revision during presentation +/// previews. Callers must invalidate it when semantic records change. +pub fn project_view_with_index( + p: &Project, + diagram: Id, + index: &crate::view_options::ViewIndex, +) -> ProjectedView { + p.diagrams + .get(&diagram) + .map_or_else(ProjectedView::default, |d| { + project_diagram_with_index(p, d, index) + }) +} /// Project alternate placement geometry without copying or mutating semantics. pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView { + let index = crate::view_options::ViewIndex::new(p); + project_diagram_with_index(p, d, &index) +} +pub fn project_diagram_with_index( + p: &Project, + d: &Diagram, + index: &crate::view_options::ViewIndex, +) -> ProjectedView { + project_diagram_impl(p, d, None, Some(index)) +} +/// Unfiltered eligibility from the same projection/anchor/fold decisions used +/// for drawing. Compute once for a view-options editor, then combine with the +/// prepared filter's reasons; hidden lines remain geometrically eligible here. +pub fn edge_availability( + p: &Project, + d: &Diagram, +) -> BTreeMap { + let mut availability = p + .relations + .keys() + .chain(p.traces.keys()) + .map(|id| { + ( + *id, + crate::view_options::EdgeAvailability::OutsideProjection, + ) + }) + .collect(); + project_diagram_impl(p, d, Some(&mut availability), None); + availability +} +fn project_diagram_impl( + p: &Project, + d: &Diagram, + mut availability: Option<&mut BTreeMap>, + index: Option<&crate::view_options::ViewIndex>, +) -> ProjectedView { let parents = parents(p, d); let mut nodes = Vec::new(); for n in d.nodes.values() { @@ -354,6 +404,7 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView { RelationKind::Interaction | RelationKind::FunctionalExchange | RelationKind::BoundaryExchange + | RelationKind::CommunicationMean ), ProjectionKind::FunctionalDataflow => matches!( r.kind(), @@ -367,20 +418,36 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView { | RelationKind::MissionCapability | RelationKind::MissionInvolvement | RelationKind::CapabilityInvolvement + | RelationKind::CommunicationMean ), _ => true, }; if !included { continue; } - if let (Some(source), Some(target)) = (anchor(r.source()), anchor(r.target())) - && (source != target - || ((d.projection.state_machine().is_some() - || r.kind() == RelationKind::DataAssociation) - && r.source() == r.target() - && nodes - .iter() - .any(|n| n.id == source && n.element == r.source()))) + let ends = (anchor(r.source()), anchor(r.target())); + let self_loop = (d.projection.state_machine().is_some() + || r.kind() == RelationKind::DataAssociation) + && r.source() == r.target() + && nodes + .iter() + .any(|n| Some(n.id) == ends.0 && n.element == r.source()); + if let Some(availability) = availability.as_deref_mut() { + use crate::view_options::EdgeAvailability as A; + availability.insert( + r.id, + match ends { + (Some(a), Some(b)) if a != b || self_loop => A::Visible, + (Some(_), Some(_)) => A::FoldedInternal, + (a, b) => A::EndpointNotShown { + source: a.is_none(), + target: b.is_none(), + }, + }, + ); + } + if let (Some(source), Some(target)) = ends + && (source != target || self_loop) { let (source_port, target_port) = match &r.body { RelationBody::FunctionalExchange { @@ -402,6 +469,9 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView { RelationBody::BoundaryExchange { oriented: false, .. + } | RelationBody::CommunicationMean { + oriented: false, + .. } | RelationBody::DataAssociation { .. } ), source_port: source_port.filter(|id| visible_ports.contains(id)), @@ -411,7 +481,22 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView { } if d.projection == ProjectionKind::Traceability { for t in p.traces.values() { - if let (Some(source), Some(target)) = (anchor(t.oa), anchor(t.sa)) + let ends = (anchor(t.oa), anchor(t.sa)); + if let Some(availability) = availability.as_deref_mut() { + use crate::view_options::EdgeAvailability as A; + availability.insert( + t.id, + match ends { + (Some(a), Some(b)) if a != b => A::Visible, + (Some(_), Some(_)) => A::FoldedInternal, + (a, b) => A::EndpointNotShown { + source: a.is_none(), + target: b.is_none(), + }, + }, + ); + } + if let (Some(source), Some(target)) = ends && source != target { edges.push(ProjectedEdge { @@ -433,6 +518,16 @@ pub fn project_diagram(p: &Project, d: &Diagram) -> ProjectedView { } else { port_layout::layout(p, d, &mut nodes, &edges) }; + // Port geometry uses every eligible edge. Presentation filters must never + // move visible ports when another line is hidden or a category is selected. + if let Some(index) = index { + let filter = if d.projection == ProjectionKind::Traceability { + index.prepare_cross_layer(d.layer, &d.view_options) + } else { + index.prepare(d.layer, &d.view_options) + }; + edges.retain(|edge| filter.includes(edge.id)); + } ProjectedView { nodes, edges, diff --git a/crates/oa-core/src/references.rs b/crates/oa-core/src/references.rs index 91721df7..10edde44 100644 --- a/crates/oa-core/src/references.rs +++ b/crates/oa-core/src/references.rs @@ -29,6 +29,11 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) { } if let Some(shared) = e.shared_mut() { match shared { + SharedBody::ExchangeCategory { exchanges, .. } => { + for id in exchanges { + visit(id); + } + } SharedBody::DataField { type_ref: TypeRef::Named(id), .. @@ -152,7 +157,9 @@ pub fn visit_relation_refs(r: &mut Relation, mut visit: impl FnMut(&mut Id)) { visit(id); } } - RelationBody::Interaction { items, .. } | RelationBody::BoundaryExchange { items, .. } => { + RelationBody::Interaction { items, .. } + | RelationBody::BoundaryExchange { items, .. } + | RelationBody::CommunicationMean { items, .. } => { for id in items { visit(id); } @@ -189,6 +196,11 @@ pub fn remap_references(p: &mut Project, old: Id, new: Id) { }); } for d in p.diagrams.values_mut() { + d.view_options.visit_references(|id| { + if *id == old { + *id = new; + } + }); if let ProjectionKind::StateMachine { machine } = &mut d.projection && *machine == old { @@ -374,6 +386,7 @@ fn remove_objects(p: &mut Project, removed: BTreeSet) { p.traces.retain(|id, _| !removed.contains(id)); p.artifact_links.retain(|id, _| !removed.contains(id)); for d in p.diagrams.values_mut() { + d.view_options.retain_references(|id| !removed.contains(id)); d.nodes.retain(|id, _| !removed.contains(id)); d.routes.retain(|route| !removed.contains(&route.edge)); for node in d.nodes.values_mut() { @@ -417,7 +430,11 @@ fn remove_objects(p: &mut Project, removed: BTreeSet) { } SharedBody::Scenario { context, .. } => clear(context), SharedBody::Lifeline { represented } => clear(represented), - SharedBody::Constraint { constrained, .. } => { + SharedBody::ExchangeCategory { + exchanges: constrained, + .. + } + | SharedBody::Constraint { constrained, .. } => { constrained.retain(|id| !removed.contains(id)) } SharedBody::Occurrence { body, .. } => match body { @@ -487,7 +504,8 @@ fn remove_objects(p: &mut Project, removed: BTreeSet) { } } RelationBody::Interaction { items, .. } - | RelationBody::BoundaryExchange { items, .. } => { + | RelationBody::BoundaryExchange { items, .. } + | RelationBody::CommunicationMean { items, .. } => { items.retain(|id| !removed.contains(id)) } RelationBody::FunctionalExchange { items, .. } => { diff --git a/crates/oa-core/src/routing.rs b/crates/oa-core/src/routing.rs index 0148db4c..16d9dd01 100644 --- a/crates/oa-core/src/routing.rs +++ b/crates/oa-core/src/routing.rs @@ -607,6 +607,7 @@ fn route_diagram_impl( .flatten(); routed.label = make_label( project, + diagram.view_options.exchange_labels, edge.id, &routed.points, authored_label, @@ -1232,6 +1233,7 @@ fn label_text(project: &Project, edge: Id) -> String { RelationKind::Interaction | RelationKind::FunctionalExchange | RelationKind::BoundaryExchange + | RelationKind::CommunicationMean | RelationKind::DataAssociation => relation.name.as_str(), RelationKind::CapabilityInclude => "«include»", RelationKind::CapabilityExtend => "«extend»", @@ -1262,8 +1264,11 @@ fn empty_label() -> EdgeLabelGeometry { } } +// Keep semantic label inputs and the independent collision sets explicit. +#[allow(clippy::too_many_arguments)] fn make_label( project: &Project, + labels: crate::view_options::ExchangeLabels, edge: Id, points: &[Position], authored: Option<&crate::EdgeLabel>, @@ -1271,7 +1276,21 @@ fn make_label( previous_labels: &[RoutingBounds], strokes: &[Segment], ) -> EdgeLabelGeometry { - let text = label_text(project, edge); + let text = if labels != crate::view_options::ExchangeLabels::Name + && project + .relations + .get(&edge) + .is_some_and(|r| crate::view_options::exchange_family(r.kind()).is_some()) + { + let label = labels.label(project, edge); + if label.chars().count() > 48 { + label.chars().take(47).chain(std::iter::once('…')).collect() + } else { + label + } + } else { + label_text(project, edge) + }; let size = Size { width: if text.is_empty() { 0.0 diff --git a/crates/oa-core/src/semantic/catalog.rs b/crates/oa-core/src/semantic/catalog.rs index 63cb4c27..a6151603 100644 --- a/crates/oa-core/src/semantic/catalog.rs +++ b/crates/oa-core/src/semantic/catalog.rs @@ -191,6 +191,7 @@ fn path(own: &Own, facet: &FacetSelector) -> Option> { F::State(StateFacet::Exit) => ("State", &["exit"]), F::State(StateFacet::DoActivity) => ("State", &["do_activity"]), F::Constraint(ConstraintFacet::Expression) => ("Constraint", &["expression"]), + F::Category(CategoryFacet::Family) => ("ExchangeCategory", &["family"]), F::Relation(RelationFacet::Source) => ("*", &["source"]), F::Relation(RelationFacet::Target) => ("*", &["target"]), F::Relation(RelationFacet::AllocationOrder) => ("InterfaceItemAllocation", &["order"]), @@ -206,7 +207,7 @@ fn path(own: &Own, facet: &FacetSelector) -> Option> { F::Relation(RelationFacet::TargetNavigable) => ("DataAssociation", &["target_navigable"]), F::Relation(RelationFacet::SourcePort) => ("FunctionalExchange", &["source_port"]), F::Relation(RelationFacet::TargetPort) => ("FunctionalExchange", &["target_port"]), - F::Relation(RelationFacet::Oriented) => ("BoundaryExchange", &["oriented"]), + F::Relation(RelationFacet::Oriented) => ("*", &["oriented"]), F::Relation(RelationFacet::ExtendCondition) => ("CapabilityExtend", &["condition"]), F::Relation(RelationFacet::SequenceGuard) => ("SequenceLink", &["guard"]), F::Relation(RelationFacet::TransitionKind) => ("StateTransition", &["transition_kind"]), @@ -311,7 +312,8 @@ pub(crate) fn collection(project: &Project, id: Id, which: ReferenceCollection) Some( RelationBody::Interaction { items, .. } | RelationBody::FunctionalExchange { items, .. } - | RelationBody::BoundaryExchange { items, .. }, + | RelationBody::BoundaryExchange { items, .. } + | RelationBody::CommunicationMean { items, .. }, ) => Some(items.clone()), _ => None, }, @@ -327,6 +329,10 @@ pub(crate) fn collection(project: &Project, id: Id, which: ReferenceCollection) Some(SharedBody::Constraint { constrained, .. }) => Some(constrained.clone()), _ => None, }, + C::CategoryExchanges => match shared { + Some(SharedBody::ExchangeCategory { exchanges, .. }) => Some(exchanges.clone()), + _ => None, + }, } } pub(crate) fn order_collection(which: OrderedCollection) -> ReferenceCollection { @@ -575,6 +581,7 @@ fn catalog_facets( ReferenceCollection::FunctionOutputItems, ReferenceCollection::SignalItems, ReferenceCollection::ExchangeItems, + ReferenceCollection::CategoryExchanges, ReferenceCollection::TransitionTriggers, ReferenceCollection::TransitionEffects, ReferenceCollection::Constrained, @@ -1036,6 +1043,11 @@ mod coverage { p }; for (kind, parts, which) in [ + ( + "ExchangeCategory", + vec!["exchanges"], + ReferenceCollection::CategoryExchanges, + ), ("Process", vec!["steps"], ReferenceCollection::Steps), ("FunctionalChain", vec!["steps"], ReferenceCollection::Steps), ("Scenario", vec!["steps"], ReferenceCollection::Steps), @@ -1079,6 +1091,11 @@ mod coverage { vec!["items"], ReferenceCollection::ExchangeItems, ), + ( + "CommunicationMean", + vec!["items"], + ReferenceCollection::ExchangeItems, + ), ( "StateTransition", vec!["triggers"], diff --git a/crates/oa-core/src/semantic/historical.rs b/crates/oa-core/src/semantic/historical.rs index 8f5e1383..400b890f 100644 --- a/crates/oa-core/src/semantic/historical.rs +++ b/crates/oa-core/src/semantic/historical.rs @@ -73,6 +73,7 @@ macro_rules! enums { } )* }; } enums!( + crate::view_options::ExchangeFamily, ExchangeItemKind, ExchangeMechanism, ParameterDirection, diff --git a/crates/oa-core/src/semantic/mutate.rs b/crates/oa-core/src/semantic/mutate.rs index 9f8ff260..dea83d0e 100644 --- a/crates/oa-core/src/semantic/mutate.rs +++ b/crates/oa-core/src/semantic/mutate.rs @@ -63,7 +63,8 @@ pub(crate) fn set_collection( Some( RelationBody::Interaction { items, .. } | RelationBody::FunctionalExchange { items, .. } - | RelationBody::BoundaryExchange { items, .. }, + | RelationBody::BoundaryExchange { items, .. } + | RelationBody::CommunicationMean { items, .. }, ) => Some(items), _ => None, }, @@ -79,6 +80,10 @@ pub(crate) fn set_collection( Some(SharedBody::Constraint { constrained, .. }) => Some(constrained), _ => None, }, + C::CategoryExchanges => match p.elements.get_mut(&id).and_then(Element::shared_mut) { + Some(SharedBody::ExchangeCategory { exchanges, .. }) => Some(exchanges), + _ => None, + }, } .ok_or("Collection is inapplicable on target")?; *slot = list; diff --git a/crates/oa-core/src/semantic/review.rs b/crates/oa-core/src/semantic/review.rs index 2f357179..c77d2f89 100644 --- a/crates/oa-core/src/semantic/review.rs +++ b/crates/oa-core/src/semantic/review.rs @@ -795,7 +795,18 @@ fn build_review( current.clone() }; if resolution.decision == SemanticDecision::TakeSource { - fact = remap_fact(&fact, &map, source_layer)?; + fact = if let Some(adapted) = transitions::review_endpoint_fact( + initial, + &plan.request.transition, + &map, + request.source, + target, + &fact, + )? { + adapted + } else { + remap_fact(&fact, &map, source_layer)? + }; } // A Keep reads the frozen target, never a value written by an earlier source. if resolution.decision != SemanticDecision::KeepTarget { diff --git a/crates/oa-core/src/semantic/types.rs b/crates/oa-core/src/semantic/types.rs index 56adee52..65e9eb6a 100644 --- a/crates/oa-core/src/semantic/types.rs +++ b/crates/oa-core/src/semantic/types.rs @@ -86,6 +86,7 @@ macro_rules! scalar_families { ExchangeItem, ExchangeItemFacet, ExchangeItemReview { Nature: ExchangeItemKind, Mechanism: ExchangeMechanism, Supertype: Option } + Category, CategoryFacet, CategoryReview { Family: crate::view_options::ExchangeFamily } Interface, InterfaceFacet, InterfaceReview { Supertype: Option } Data, DataFacet, DataReview { Constructor: DataConstructor, @@ -197,6 +198,7 @@ pub enum ReferenceCollection { FunctionOutputItems, SignalItems, ExchangeItems, + CategoryExchanges, TransitionTriggers, TransitionEffects, Constrained, diff --git a/crates/oa-core/src/source_files.rs b/crates/oa-core/src/source_files.rs index 4686559d..524ec44e 100644 --- a/crates/oa-core/src/source_files.rs +++ b/crates/oa-core/src/source_files.rs @@ -9,6 +9,13 @@ //! may remain after interruption during staging/completed cleanup. They contain //! no pending transaction unless named `.arcadia-save-ENTRY.journal`. Windows //! has no portable directory fsync; this is not a power-loss durability promise. +//! Journal v2 records whether writes may have begun with a flushed marker. +//! Recovery of a v2 journal without that marker discards only journal metadata; +//! v1 journals retain their prior rollback rules. +//! Journal publication refuses replacement on Windows and Linux, including a +//! competing empty directory. Linux requires kernel/filesystem support for +//! `renameat2(RENAME_NOREPLACE)` and fails without a fallback when unavailable. +//! Other operating systems retain standard rename semantics. use crate::{language, source_project::*}; use serde::{Deserialize, Serialize}; use std::{ @@ -22,6 +29,8 @@ use thiserror::Error; const MAX_PATH_BYTES: usize = 4096; const MAX_JOURNAL_BYTES: usize = 32 * 1024 * 1024; const MAX_JOURNAL_FILES: usize = 4096; +const WRITE_STARTED: &str = "write-started"; +const WRITE_STARTED_BYTES: &[u8] = b"Arcadia journal v2: model writes may have started\n"; #[derive(Debug, Clone, PartialEq, Eq)] pub enum DiskExpectation { @@ -588,6 +597,127 @@ fn staged_path(journal: &Path, index: usize, suffix: &str) -> PathBuf { journal.join(format!("{index}.{suffix}")) } +fn publish_journal(stage: &Path, journal: &Path) -> Result<(), SourceFileError> { + #[cfg(windows)] + { + publish_journal_with_retry( + stage, + journal, + rename_journal_without_replacement, + std::thread::sleep, + ) + } + #[cfg(target_os = "linux")] + { + rename_journal_without_replacement(stage, journal).map_err(|error| io_at(journal, error)) + } + // Other platforms retain their existing rename behavior; the native + // no-replacement publication guarantee is currently Windows/Linux only. + #[cfg(not(any(windows, target_os = "linux")))] + { + fs::rename(stage, journal).map_err(|error| io_at(journal, error)) + } +} + +#[cfg(target_os = "linux")] +fn rename_journal_without_replacement(stage: &Path, journal: &Path) -> io::Result<()> { + use std::{ffi::CString, os::unix::ffi::OsStrExt}; + + let path_bytes = |path: &Path| { + CString::new(path.as_os_str().as_bytes()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + "Journal path contains an interior NUL.", + ) + }) + }; + let (stage, journal) = (path_bytes(stage)?, path_bytes(journal)?); + // Use libc's architecture-specific syscall number, avoiding a dependency on + // the newer glibc renameat2 wrapper. Both NUL-terminated byte buffers remain + // alive for the call. RENAME_NOREPLACE makes even an empty competing directory + // an error. Unsupported kernels/filesystems fail closed: never fall back to + // ordinary rename, whose replacement semantics would lose journal ownership. + let result = unsafe { + libc::syscall( + libc::SYS_renameat2, + libc::AT_FDCWD as libc::c_long, + stage.as_ptr(), + libc::AT_FDCWD as libc::c_long, + journal.as_ptr(), + libc::RENAME_NOREPLACE as libc::c_ulong, + ) + }; + if result == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +#[cfg(windows)] +fn rename_journal_without_replacement(stage: &Path, journal: &Path) -> io::Result<()> { + use std::os::windows::ffi::OsStrExt; + #[link(name = "kernel32")] + unsafe extern "system" { + fn MoveFileExW(existing: *const u16, destination: *const u16, flags: u32) -> i32; + } + let wide = |path: &Path| -> io::Result> { + let mut value: Vec<_> = path.as_os_str().encode_wide().collect(); + if value.contains(&0) { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Journal path contains an interior NUL.", + )); + } + value.push(0); + Ok(value) + }; + let (stage, journal) = (wide(stage)?, wide(journal)?); + // Both terminated UTF-16 buffers live through the call. Flags=0 requests + // neither replacement, cross-volume copying nor deferred execution. Unlike + // fs::rename on modern Windows, an existing empty directory is not replaced. + if unsafe { MoveFileExW(stage.as_ptr(), journal.as_ptr(), 0) } == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +// Windows scanners may briefly open newly written stage files without delete +// sharing. Retry only publication, before any model file has been replaced. +// Ownership still transfers only after one successful rename. Never overwrite +// a journal that appeared while waiting, and retain the final OS error/path. +#[cfg(any(windows, test))] +fn publish_journal_with_retry( + stage: &Path, + journal: &Path, + mut rename: impl FnMut(&Path, &Path) -> io::Result<()>, + mut pause: impl FnMut(std::time::Duration), +) -> Result<(), SourceFileError> { + let mut delays = [5, 10, 25, 50, 100, 200].into_iter(); + loop { + match fs::symlink_metadata(journal) { + Ok(_) => return Err(SourceFileError::RecoveryRequired(journal.into())), + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(io_at(journal, error)), + } + match rename(stage, journal) { + Ok(()) => return Ok(()), + Err(error) => { + // ERROR_ACCESS_DENIED, ERROR_SHARING_VIOLATION, ERROR_LOCK_VIOLATION. + // A permanent ACL failure is still returned after at most 390 ms. + if matches!(error.raw_os_error(), Some(5 | 32 | 33)) + && let Some(delay) = delays.next() + { + pause(std::time::Duration::from_millis(delay)); + continue; + } + return Err(io_at(journal, error)); + } + } + } +} + fn write_synced(path: &Path, bytes: &[u8]) -> Result<(), SourceFileError> { let mut file = File::options() .write(true) @@ -699,6 +829,26 @@ fn save_with_hook( compiled: &CompiledSourceProject, limits: ProjectLimits, mut after_replace: impl FnMut(usize) -> io::Result<()>, +) -> Result { + save_with_phases(snapshot, compiled, limits, |phase| match phase { + SavePhase::AfterReplace(count) => after_replace(count), + _ => Ok(()), + }) +} + +enum SavePhase { + BeforePublish, + AfterPublish, + AfterWriteMarker, + AfterReplace(usize), + BeforeRetire, +} + +fn save_with_phases( + snapshot: &DiskProject, + compiled: &CompiledSourceProject, + limits: ProjectLimits, + mut phase: impl FnMut(SavePhase) -> io::Result<()>, ) -> Result { check_snapshot(snapshot, limits)?; let checked = compile_bundle(&compiled.bundle, limits).map_err(|diagnostics| { @@ -739,7 +889,7 @@ fn save_with_hook( // Stable dependency order, with the project entry committed last. entries.sort_by_key(|file| (file.path == snapshot.bundle.entry, file.path.clone())); let journal = Journal { - version: 1, + version: 2, entry: snapshot.bundle.entry.clone(), files: entries, }; @@ -775,20 +925,46 @@ fn save_with_hook( } write_synced(&stage.path().join("journal.json"), &metadata)?; sync_directory(stage.path())?; + phase(SavePhase::BeforePublish).map_err(|error| io_at(&journal_dir, error))?; // Publishing a complete directory makes interrupted staging harmless. - fs::rename(stage.path(), &journal_dir).map_err(|error| io_at(&journal_dir, error))?; + publish_journal(stage.path(), &journal_dir)?; let _ = stage.keep(); // The renamed directory now belongs to recovery. sync_directory(&snapshot.root)?; + phase(SavePhase::AfterPublish).map_err(|error| io_at(&journal_dir, error))?; + // Publication may have waited while another writer completed the same + // candidate. Nothing has been written by this save: never interpret their + // matching after-bytes as ours and roll them back. The absent v2 marker + // also preserves this distinction after interruption or retirement failure. + if let Err(error) = check_conflicts(&snapshot.root, &snapshot.expectations) { + let retirement = phase(SavePhase::BeforeRetire) + .map_err(|error| io_at(&journal_dir, error)) + .and_then(|()| retire_journal(&snapshot.root, &journal_dir, &journal)); + return match retirement { + Ok(()) => Err(error), + Err(retirement) => Err(SourceFileError::RecoveryFailed { + save: error.to_string(), + recovery: retirement.to_string(), + journal: journal_dir, + }), + }; + } + write_synced(&journal_dir.join(WRITE_STARTED), WRITE_STARTED_BYTES)?; + sync_directory(&journal_dir)?; + phase(SavePhase::AfterWriteMarker).map_err(|error| io_at(&journal_dir, error))?; let result = (|| { let mut current = snapshot.expectations.clone(); let mut replacements = 0; for file in journal.files.iter().filter(|file| file.write) { - check_conflicts(&snapshot.root, ¤t)?; + // The first check is above, before the durable write-attempt marker. + // Further checks follow real write attempts and may need rollback. + if replacements != 0 { + check_conflicts(&snapshot.root, ¤t)?; + } let bytes = compiled.bundle.files[&file.path].as_bytes(); atomic_write(&snapshot.root, &file.path, bytes)?; current.insert(file.path.clone(), DiskExpectation::Bytes(bytes.to_vec())); replacements += 1; - after_replace(replacements) + phase(SavePhase::AfterReplace(replacements)) .map_err(|error| io_at(&snapshot.root.join(&file.path), error))?; } check_conflicts(&snapshot.root, ¤t)?; @@ -804,6 +980,7 @@ fn save_with_hook( }), }; } + phase(SavePhase::BeforeRetire).map_err(|error| io_at(&journal_dir, error))?; retire_journal(&snapshot.root, &journal_dir, &journal)?; Ok(saved_snapshot(snapshot, compiled)) } @@ -833,6 +1010,14 @@ fn remove_journal(directory: &Path, journal: &Journal) -> Result<(), SourceFileE } } } + if journal.version == 2 { + let marker = directory.join(WRITE_STARTED); + match fs::remove_file(&marker) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => {} + Err(error) => return Err(io_at(&marker, error)), + } + } let metadata = directory.join("journal.json"); fs::remove_file(&metadata).map_err(|error| io_at(&metadata, error))?; fs::remove_dir(directory).map_err(|error| io_at(directory, error)) @@ -898,7 +1083,10 @@ fn recover_with_hook( let bytes = read_journal_file(&metadata_path, MAX_JOURNAL_BYTES)?; let journal: Journal = serde_json::from_slice(&bytes) .map_err(|error| SourceFileError::Invalid(format!("Invalid recovery journal: {error}")))?; - if journal.version != 1 || journal.entry != entry || journal.files.len() > limits.max_files { + if !matches!(journal.version, 1 | 2) + || journal.entry != entry + || journal.files.len() > limits.max_files + { return Err(SourceFileError::Invalid( "Recovery journal version, entry or file budget is invalid.".into(), )); @@ -908,6 +1096,25 @@ fn recover_with_hook( let mut folded = BTreeSet::new(); let (mut before_bytes, mut after_bytes) = (0usize, 0usize); let mut allowed = BTreeSet::from(["journal.json".to_string()]); + let write_started = if journal.version == 2 { + let marker = directory.join(WRITE_STARTED); + match fs::symlink_metadata(&marker) { + Ok(_) => { + if read_journal_file(&marker, WRITE_STARTED_BYTES.len())? != WRITE_STARTED_BYTES { + return Err(SourceFileError::Invalid( + "Invalid journal write-started marker.".into(), + )); + } + allowed.insert(WRITE_STARTED.into()); + true + } + Err(error) if error.kind() == io::ErrorKind::NotFound => false, + Err(error) => return Err(io_at(&marker, error)), + } + } else { + // Frozen v1 journals predate the marker and retain their rollback rules. + true + }; for (index, file) in journal.files.iter().enumerate() { disk_path(&root, &file.path)?; if !folded.insert(file.path.to_lowercase()) { @@ -952,6 +1159,10 @@ fn recover_with_hook( )); } } + if !write_started { + retire_journal(&root, &directory, &journal)?; + return Ok(true); + } let mut current = BTreeMap::new(); let mut conflicts = Vec::new(); for (path, before) in &original { @@ -995,6 +1206,342 @@ fn recover_with_hook( mod transaction_tests { use super::*; + #[cfg(target_os = "linux")] + #[test] + fn linux_journal_publication_preserves_competing_destinations_and_our_stage() { + use std::os::unix::{fs::MetadataExt, fs::symlink}; + + for kind in ["empty directory", "nonempty directory", "file", "symlink"] { + let directory = tempfile::tempdir().unwrap(); + let stage = directory.path().join("stage"); + let journal = directory.path().join("journal"); + fs::create_dir(&stage).unwrap(); + fs::write(stage.join("journal.json"), b"our complete metadata").unwrap(); + // Simulate a competitor arriving after the caller's absence check. + assert!(!journal.exists()); + match kind { + "empty directory" => fs::create_dir(&journal).unwrap(), + "nonempty directory" => { + fs::create_dir(&journal).unwrap(); + fs::write(journal.join("journal.json"), b"competitor metadata").unwrap(); + } + "file" => fs::write(&journal, b"competitor file").unwrap(), + "symlink" => symlink("absent competitor target", &journal).unwrap(), + _ => unreachable!(), + } + let before = fs::symlink_metadata(&journal).unwrap(); + let error = publish_journal(&stage, &journal).unwrap_err(); + assert!( + matches!(error, SourceFileError::Io { path, source } + if path == journal && source.raw_os_error() == Some(libc::EEXIST)), + "{kind} must refuse publication" + ); + let after = fs::symlink_metadata(&journal).unwrap(); + assert_eq!((before.dev(), before.ino()), (after.dev(), after.ino())); + assert_eq!( + fs::read(stage.join("journal.json")).unwrap(), + b"our complete metadata" + ); + match kind { + "empty directory" => assert_eq!(fs::read_dir(&journal).unwrap().count(), 0), + "nonempty directory" => assert_eq!( + fs::read(journal.join("journal.json")).unwrap(), + b"competitor metadata" + ), + "file" => assert_eq!(fs::read(&journal).unwrap(), b"competitor file"), + "symlink" => assert_eq!( + fs::read_link(&journal).unwrap(), + Path::new("absent competitor target") + ), + _ => unreachable!(), + } + } + } + + #[cfg(target_os = "linux")] + #[test] + fn linux_journal_publication_preserves_raw_path_bytes_and_rejects_nul() { + use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; + + let directory = tempfile::tempdir().unwrap(); + let stage = directory.path().join(OsStr::from_bytes(b"stage-\xff")); + let journal = directory.path().join(OsStr::from_bytes(b"journal-\xfe")); + fs::create_dir(&stage).unwrap(); + fs::write(stage.join("journal.json"), b"complete metadata").unwrap(); + for (a, b) in [ + (Path::new("bad\0path"), journal.as_path()), + (stage.as_path(), Path::new("bad\0path")), + ] { + let error = rename_journal_without_replacement(a, b).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidInput); + assert!(stage.join("journal.json").exists()); + assert!(!journal.exists()); + } + publish_journal(&stage, &journal).unwrap(); + assert!(!stage.exists()); + assert_eq!( + fs::read(journal.join("journal.json")).unwrap(), + b"complete metadata" + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn linux_journal_publication_has_one_winner_for_concurrent_stages() { + use std::sync::Barrier; + + let directory = tempfile::tempdir().unwrap(); + let journal = directory.path().join("journal"); + let stages = [ + directory.path().join("stage-a"), + directory.path().join("stage-b"), + ]; + for (i, stage) in stages.iter().enumerate() { + fs::create_dir(stage).unwrap(); + fs::write(stage.join("journal.json"), format!("complete writer {i}")).unwrap(); + } + let barrier = Barrier::new(2); + let results = std::thread::scope(|scope| { + let journal = &journal; + let barrier = &barrier; + let handles: Vec<_> = stages + .iter() + .map(|stage| { + scope.spawn(move || { + assert!(!journal.exists()); + barrier.wait(); + publish_journal(stage, journal) + }) + }) + .collect(); + handles + .into_iter() + .map(|handle| handle.join().unwrap()) + .collect::>() + }); + assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1); + for (i, result) in results.into_iter().enumerate() { + match result { + Ok(()) => { + assert!(!stages[i].exists()); + assert_eq!( + fs::read_to_string(journal.join("journal.json")).unwrap(), + format!("complete writer {i}") + ); + } + Err(SourceFileError::Io { path, source }) => { + assert_eq!(path, journal); + assert_eq!(source.raw_os_error(), Some(libc::EEXIST)); + assert_eq!( + fs::read_to_string(stages[i].join("journal.json")).unwrap(), + format!("complete writer {i}") + ); + } + Err(error) => panic!("unexpected publication error: {error}"), + } + } + } + + #[cfg(target_os = "linux")] + #[test] + #[ignore = "requires strace renameat2 error injection and OA_TEST_RENAMEAT2_ERRNO"] + fn linux_journal_publication_injected_unsupported_error_is_fail_closed() { + let errno: i32 = std::env::var("OA_TEST_RENAMEAT2_ERRNO") + .expect("set OA_TEST_RENAMEAT2_ERRNO to the injected Linux errno") + .parse() + .unwrap(); + assert!(matches!( + errno, + libc::ENOSYS | libc::EINVAL | libc::EOPNOTSUPP + )); + let (directory, snapshot, compiled) = fixture(); + let journal = journal_path(&snapshot.root, &snapshot.bundle.entry); + let original: Vec<_> = snapshot + .bundle + .files + .keys() + .map(|path| (path.clone(), fs::read(directory.path().join(path)).unwrap())) + .collect(); + let error = save_with_phases(&snapshot, &compiled, ProjectLimits::default(), |phase| { + if matches!( + phase, + SavePhase::AfterPublish | SavePhase::AfterWriteMarker | SavePhase::AfterReplace(_) + ) { + panic!("unsupported no-replace publication must stop before ownership transfer"); + } + Ok(()) + }) + .unwrap_err(); + assert!(matches!(error, SourceFileError::Io { path, source } + if path == journal && source.raw_os_error() == Some(errno))); + assert!(!journal.exists()); + for (path, bytes) in original { + assert_eq!(fs::read(directory.path().join(path)).unwrap(), bytes); + } + check_conflicts(&snapshot.root, &snapshot.expectations).unwrap(); + } + + #[cfg(target_os = "linux")] + #[test] + fn linux_journal_publication_refusal_never_starts_model_writes() { + let (directory, snapshot, compiled) = fixture(); + let journal = journal_path(&snapshot.root, &snapshot.bundle.entry); + let original: Vec<_> = snapshot + .bundle + .files + .keys() + .map(|path| (path.clone(), fs::read(directory.path().join(path)).unwrap())) + .collect(); + let error = save_with_phases(&snapshot, &compiled, ProjectLimits::default(), |phase| { + match phase { + SavePhase::BeforePublish => fs::create_dir(&journal).unwrap(), + SavePhase::AfterPublish + | SavePhase::AfterWriteMarker + | SavePhase::AfterReplace(_) => { + panic!("a rejected publisher must not obtain ownership or write source files") + } + _ => {} + } + Ok(()) + }) + .unwrap_err(); + assert!(matches!(error, SourceFileError::Io { path, source } + if path == journal && source.raw_os_error() == Some(libc::EEXIST))); + assert_eq!(fs::read_dir(&journal).unwrap().count(), 0); + assert!(!journal.join(WRITE_STARTED).exists()); + for (path, bytes) in original { + assert_eq!(fs::read(directory.path().join(path)).unwrap(), bytes); + } + check_conflicts(&snapshot.root, &snapshot.expectations).unwrap(); + } + + #[cfg(windows)] + #[test] + fn journal_publication_does_not_replace_empty_target_created_after_precheck() { + let directory = tempfile::tempdir().unwrap(); + let stage = directory.path().join("stage"); + let journal = directory.path().join("journal"); + fs::create_dir(&stage).unwrap(); + fs::write(stage.join("journal.json"), b"our journal").unwrap(); + let mut called = false; + let result = publish_journal_with_retry( + &stage, + &journal, + |a, b| { + assert!(!called); + called = true; + fs::create_dir(b).unwrap(); + rename_journal_without_replacement(a, b) + }, + |_| {}, + ); + assert!(result.is_err()); + assert!(called); + assert_eq!( + fs::read(stage.join("journal.json")).unwrap(), + b"our journal" + ); + assert_eq!(fs::read_dir(&journal).unwrap().count(), 0); + assert!( + matches!(rename_journal_without_replacement(Path::new("bad\0path"), &journal), Err(error) if error.kind() == io::ErrorKind::InvalidInput) + ); + } + + #[test] + fn journal_publication_retries_only_transient_windows_errors_before_transferring_ownership() { + let directory = tempfile::tempdir().unwrap(); + let stage = directory.path().join("stage"); + let journal = directory.path().join("journal"); + fs::create_dir(&stage).unwrap(); + fs::write(stage.join("journal.json"), b"complete staged metadata").unwrap(); + let mut errors = [5, 32, 33].into_iter(); + let mut waits = vec![]; + publish_journal_with_retry( + &stage, + &journal, + |a, b| { + assert!(a.join("journal.json").exists()); + assert!(!b.exists()); + match errors.next() { + Some(code) => Err(io::Error::from_raw_os_error(code)), + None => fs::rename(a, b), + } + }, + |delay| waits.push(delay.as_millis()), + ) + .unwrap(); + assert_eq!(waits, [5, 10, 25]); + assert!(!stage.exists()); + assert_eq!( + fs::read(journal.join("journal.json")).unwrap(), + b"complete staged metadata" + ); + } + + #[test] + fn journal_publication_exhaustion_and_permanent_errors_keep_stage_and_report_original_path() { + for (code, expected_calls, expected_wait) in [(5, 7, 390), (123, 1, 0)] { + let directory = tempfile::tempdir().unwrap(); + let stage = directory.path().join("stage"); + let journal = directory.path().join("journal"); + fs::create_dir(&stage).unwrap(); + fs::write(stage.join("journal.json"), b"before publication").unwrap(); + let (mut calls, mut waited) = (0, 0); + let error = publish_journal_with_retry( + &stage, + &journal, + |_, _| { + calls += 1; + Err(io::Error::from_raw_os_error(code)) + }, + |delay| waited += delay.as_millis(), + ) + .unwrap_err(); + assert_eq!((calls, waited), (expected_calls, expected_wait)); + assert!( + matches!(error, SourceFileError::Io { path, source } if path == journal && source.raw_os_error() == Some(code)) + ); + assert_eq!( + fs::read(stage.join("journal.json")).unwrap(), + b"before publication" + ); + assert!(!journal.exists()); + } + } + + #[test] + fn journal_publication_never_replaces_another_journal_that_appears_during_retry() { + let directory = tempfile::tempdir().unwrap(); + let stage = directory.path().join("stage"); + let journal = directory.path().join("journal"); + fs::create_dir(&stage).unwrap(); + fs::write(stage.join("journal.json"), b"our staged journal").unwrap(); + let mut calls = 0; + let error = publish_journal_with_retry( + &stage, + &journal, + |_, _| { + calls += 1; + Err(io::Error::from_raw_os_error(32)) + }, + |_| { + fs::create_dir(&journal).unwrap(); + fs::write(journal.join("journal.json"), b"other save owns this").unwrap(); + }, + ) + .unwrap_err(); + assert_eq!(calls, 1); + assert!(matches!(error, SourceFileError::RecoveryRequired(path) if path == journal)); + assert_eq!( + fs::read(journal.join("journal.json")).unwrap(), + b"other save owns this" + ); + assert_eq!( + fs::read(stage.join("journal.json")).unwrap(), + b"our staged journal" + ); + } + fn fixture() -> (tempfile::TempDir, DiskProject, CompiledSourceProject) { let directory = tempfile::tempdir().unwrap(); fs::write( @@ -1014,6 +1561,160 @@ mod transaction_tests { (directory, snapshot, compiled) } + #[test] + fn unwritten_journal_never_rolls_back_a_completed_writer_with_identical_after_bytes() { + for interrupt_retirement in [false, true] { + let (directory, snapshot, compiled) = fixture(); + let error = save_with_phases(&snapshot, &compiled, ProjectLimits::default(), |phase| { + match phase { + SavePhase::BeforePublish => { + // A competing save finishes during our publication wait. + save_project(&snapshot, &compiled, ProjectLimits::default()).unwrap(); + } + SavePhase::BeforeRetire if interrupt_retirement => { + return Err(io::Error::other("injected no-write retirement failure")); + } + SavePhase::AfterWriteMarker | SavePhase::AfterReplace(_) => { + panic!("losing writer must not start writes") + } + _ => {} + } + Ok(()) + }) + .unwrap_err(); + assert!(matches!( + error, + SourceFileError::Conflict(_) | SourceFileError::RecoveryFailed { .. } + )); + for (path, bytes) in &compiled.bundle.files { + assert_eq!( + fs::read_to_string(directory.path().join(path)).unwrap(), + *bytes + ); + } + let journal = journal_path(&snapshot.root, &snapshot.bundle.entry); + assert_eq!(journal.exists(), interrupt_retirement); + if interrupt_retirement { + assert!(!journal.join(WRITE_STARTED).exists()); + // Explicit recovery after a failed retirement must also leave + // the completed other writer untouched, even matching after bytes. + assert!( + recover_project( + &directory.path().join("project.arcadia"), + ProjectLimits::default() + ) + .unwrap() + ); + for (path, bytes) in &compiled.bundle.files { + assert_eq!( + fs::read_to_string(directory.path().join(path)).unwrap(), + *bytes + ); + } + } + assert!(!journal.exists()); + } + } + + #[test] + fn recovery_before_any_write_accepts_v2_marker_and_keeps_v1_rollback_compatibility() { + for version in [1, 2] { + let (directory, snapshot, compiled) = fixture(); + let interrupted = std::panic::catch_unwind(|| { + let _ = save_with_phases(&snapshot, &compiled, ProjectLimits::default(), |phase| { + if matches!(phase, SavePhase::AfterWriteMarker) { + panic!("interrupted before atomic write"); + } + Ok(()) + }); + }); + assert!(interrupted.is_err()); + check_conflicts(&snapshot.root, &snapshot.expectations).unwrap(); + let journal = journal_path(&snapshot.root, &snapshot.bundle.entry); + assert_eq!( + fs::read(journal.join(WRITE_STARTED)).unwrap(), + WRITE_STARTED_BYTES + ); + if version == 1 { + // A frozen v1 journal has no marker and means writes may have begun. + let path = journal.join("journal.json"); + let mut metadata: Journal = + serde_json::from_slice(&fs::read(&path).unwrap()).unwrap(); + metadata.version = 1; + fs::write(path, serde_json::to_vec(&metadata).unwrap()).unwrap(); + fs::remove_file(journal.join(WRITE_STARTED)).unwrap(); + for (path, bytes) in &compiled.bundle.files { + fs::write(directory.path().join(path), bytes).unwrap(); + } + } + assert!( + recover_project( + &directory.path().join("project.arcadia"), + ProjectLimits::default() + ) + .unwrap() + ); + check_conflicts(&snapshot.root, &snapshot.expectations).unwrap(); + } + } + + #[test] + fn interrupted_publication_without_write_marker_never_rolls_back_external_matching_bytes() { + let (directory, snapshot, compiled) = fixture(); + let interrupted = std::panic::catch_unwind(|| { + let _ = save_with_phases(&snapshot, &compiled, ProjectLimits::default(), |phase| { + if matches!(phase, SavePhase::AfterPublish) { + panic!("interrupted before write marker"); + } + Ok(()) + }); + }); + assert!(interrupted.is_err()); + let journal = journal_path(&snapshot.root, &snapshot.bundle.entry); + assert!(!journal.join(WRITE_STARTED).exists()); + for (path, bytes) in &compiled.bundle.files { + fs::write(directory.path().join(path), bytes).unwrap(); + } + assert!( + recover_project( + &directory.path().join("project.arcadia"), + ProjectLimits::default() + ) + .unwrap() + ); + for (path, bytes) in &compiled.bundle.files { + assert_eq!( + fs::read_to_string(directory.path().join(path)).unwrap(), + *bytes + ); + } + assert!(!journal.exists()); + } + + #[test] + fn malformed_v2_write_marker_refuses_recovery_without_changing_source_bytes() { + let (directory, snapshot, compiled) = fixture(); + let _ = std::panic::catch_unwind(|| { + let _ = save_with_phases(&snapshot, &compiled, ProjectLimits::default(), |phase| { + if matches!(phase, SavePhase::AfterWriteMarker) { + panic!("interrupted"); + } + Ok(()) + }); + }); + let journal = journal_path(&snapshot.root, &snapshot.bundle.entry); + fs::write(journal.join(WRITE_STARTED), b"partial").unwrap(); + assert!(matches!( + recover_project( + &directory.path().join("project.arcadia"), + ProjectLimits::default() + ), + Err(SourceFileError::Invalid(_)) + )); + check_conflicts(&snapshot.root, &snapshot.expectations).unwrap(); + assert!(journal.exists()); + } + #[test] fn partial_save_failures_restore_every_original_and_write_entry_last() { for stop in 1..=3 { diff --git a/crates/oa-core/src/transitions.rs b/crates/oa-core/src/transitions.rs index e57e10a2..a56439f8 100644 --- a/crates/oa-core/src/transitions.rs +++ b/crates/oa-core/src/transitions.rs @@ -1,6 +1,8 @@ use crate::*; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; +mod communication; +pub(crate) use communication::review_endpoint_fact; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum TransitionDirection { @@ -53,6 +55,30 @@ pub struct FieldResolution { pub field: TransitionField, pub decision: FieldDecision, } +/// Technical endpoint choices are scoped to one OA exchange. Participant +/// correspondence remains in `mappings`, never in these port identities. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum CommunicationPortChoice { + Reuse { + port: Id, + }, + Create { + id: Id, + alias: String, + name: String, + description: String, + kind: ComponentPortKind, + orientation: ComponentPortOrientation, + }, +} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CommunicationEndpointDecision { + pub source: Id, + pub source_port: CommunicationPortChoice, + pub target_port: CommunicationPortChoice, +} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TransitionRequest { @@ -63,6 +89,8 @@ pub struct TransitionRequest { pub fields: Vec, pub resolutions: Vec, pub destination_diagram: Option, + #[serde(default)] + pub communication_endpoints: Vec, } impl TransitionRequest { pub fn new(direction: TransitionDirection, sources: Vec) -> Self { @@ -74,6 +102,7 @@ impl TransitionRequest { fields: vec![TransitionField::Name, TransitionField::Description], resolutions: vec![], destination_diagram: None, + communication_endpoints: vec![], } } } @@ -177,6 +206,7 @@ fn inferred_kind(kind: Kind, direction: TransitionDirection) -> Option { || matches!( k, Kind::ExchangeItem + | Kind::ExchangeCategory | Kind::DataType | Kind::DataField | Kind::Interface @@ -255,6 +285,28 @@ fn mapped_relation(r: &Relation, direction: TransitionDirection) -> Option RelationBody::BoundaryExchange { + source, + target, + oriented: *oriented, + items: items.clone(), + }, + ( + RelationBody::BoundaryExchange { + oriented, items, .. + }, + TransitionDirection::SaToOa, + ) => RelationBody::CommunicationMean { + source, + target, + oriented: *oriented, + items: items.clone(), + }, ( RelationBody::Allocation { .. } | RelationBody::InterfaceItemAllocation { .. } @@ -318,7 +370,7 @@ fn trace_kind(p: &Project, oa: Id, sa: Id) -> TraceKind { _ => TraceKind::Manual, }, _ => { - if matches!((p.relations.get(&oa),p.relations.get(&sa)),(Some(a),Some(b))if a.kind()==RelationKind::Interaction&&b.kind()==RelationKind::FunctionalExchange) + if matches!((p.relations.get(&oa),p.relations.get(&sa)),(Some(a),Some(b))if exchange_counterparts(a.kind(), b.kind())) { TraceKind::Exchange } else { @@ -349,11 +401,12 @@ fn sa_port_omissions(p: &Project) -> BTreeSet { .collect(); loop { let before = omitted.len(); - for r in p - .relations - .values() - .filter(|r| r.kind() != RelationKind::FunctionalExchange) - { + for r in p.relations.values().filter(|r| { + !matches!( + r.kind(), + RelationKind::FunctionalExchange | RelationKind::BoundaryExchange + ) + }) { if crate::references::relation_references(r) .iter() .any(|id| omitted.contains(id)) @@ -499,6 +552,15 @@ pub fn plan_transition( } } else if let Some(r) = project.relations.get(&id) { if request.direction == TransitionDirection::SaToOa + && r.kind() == RelationKind::BoundaryExchange + { + selected.extend( + [r.source(), r.target()].into_iter().filter_map(|port| { + project.elements.get(&port).and_then(|e| e.parent) + }), + ); + selected.extend(r.body.items()); + } else if request.direction == TransitionDirection::SaToOa && let RelationBody::FunctionalExchange { source, target, @@ -695,6 +757,8 @@ pub fn plan_transition( } } let mut working = project.clone(); + let communication_pairs = + communication::prepare_ports(project, &mut working, &targets, &mut plan); let ordered_creations: Vec<_> = selected .iter() .filter(|id| creating.contains(id) && project.elements.contains_key(id)) @@ -764,6 +828,25 @@ pub fn plan_transition( let mut created = r.clone(); created.id = target; created.body = body; + // SA technical ports project through their reviewed owners; never + // place a port→participant pair in the global correspondence map. + if request.direction == TransitionDirection::SaToOa + && r.kind() == RelationKind::BoundaryExchange + { + let owners = [r.source(), r.target()] + .map(|port| project.elements.get(&port).and_then(|e| e.parent)); + if let [Some(a), Some(b)] = owners { + created.body.set_endpoints(a, b); + } else { + conflict( + &mut plan, + Some(*source), + Some(target), + None, + "Boundary endpoints need participant owners before communication transition.", + ); + } + } created.alias = working.fresh_alias( &format!( "{}_{}", @@ -778,6 +861,21 @@ pub fn plan_transition( *id = *mapped; } }); + if request.direction == TransitionDirection::OaToSa + && r.kind() == RelationKind::CommunicationMean + { + if let Some((a, b)) = communication_pairs.get(source) { + created.body.set_endpoints(*a, *b); + } else { + conflict( + &mut plan, + Some(*source), + Some(target), + None, + "Choose an explicit SA component-port pair for this communication means; participant mappings cannot stand in for ports.", + ); + } + } for dependency in crate::references::relation_references(&created) { if dependency.is_nil() && request.direction == TransitionDirection::OaToSa @@ -1139,9 +1237,22 @@ fn compatible_existing( (Some(a), Some(b)) => compatible_kinds(a.kind(), b.kind(), direction), _ => match (p.relations.get(&source), p.relations.get(&target)) { (Some(a), Some(b)) => { - mapped_relation(a, direction).is_some_and(|body| body.kind() == b.kind()) + (a.kind() == RelationKind::BoundaryExchange + && b.kind() == RelationKind::BoundaryExchange) + || mapped_relation(a, direction).is_some_and(|body| body.kind() == b.kind()) } _ => false, }, } } + +pub(crate) fn exchange_counterparts(oa: RelationKind, sa: RelationKind) -> bool { + matches!( + (oa, sa), + (RelationKind::Interaction, RelationKind::FunctionalExchange) + | ( + RelationKind::CommunicationMean, + RelationKind::BoundaryExchange + ) + ) +} diff --git a/crates/oa-core/src/transitions/communication.rs b/crates/oa-core/src/transitions/communication.rs new file mode 100644 index 00000000..500a571d --- /dev/null +++ b/crates/oa-core/src/transitions/communication.rs @@ -0,0 +1,210 @@ +use super::*; + +fn port_id(choice: &CommunicationPortChoice) -> Id { + match choice { + CommunicationPortChoice::Reuse { port } => *port, + CommunicationPortChoice::Create { id, .. } => *id, + } +} + +pub(super) fn prepare_ports( + original: &Project, + working: &mut Project, + targets: &BTreeMap, + plan: &mut TransitionPlan, +) -> BTreeMap { + let mut pairs = BTreeMap::new(); + for decision in plan.request.communication_endpoints.clone() { + let result = (|| -> Result<(Id, Id), String> { + if plan.request.direction != TransitionDirection::OaToSa { + return Err( + "SA endpoint choices apply only to OA→SA communication transition.".into(), + ); + } + let means = original + .relations + .get(&decision.source) + .filter(|r| r.kind() == RelationKind::CommunicationMean) + .ok_or("Endpoint decision must select an OA communication means.")?; + if !plan.request.sources.contains(&means.id) && !targets.contains_key(&means.id) { + return Err("Endpoint decision is outside this transition selection.".into()); + } + if pairs.contains_key(&decision.source) { + return Err("Choose one component-port pair per communication means.".into()); + } + let mut pending = vec![]; + for (participant, choice) in [ + (means.source(), &decision.source_port), + (means.target(), &decision.target_port), + ] { + let owner = targets.get(&participant).copied().ok_or("Review each OA participant's SA counterpart before selecting its component port.")?; + if working.elements.get(&owner).is_some_and(|e| { + e.layer() != Layer::Sa || !matches!(e.kind(), Kind::System | Kind::SystemActor) + }) { + return Err( + "Component-port owner must be the reviewed SA participant counterpart." + .into(), + ); + } + match choice { + CommunicationPortChoice::Reuse { port } => { + let e = working + .elements + .get(port) + .ok_or("Selected component port does not exist.")?; + if e.parent != Some(owner) + || e.layer() != Layer::Sa + || !matches!( + crate::ports::port_contract(e), + Some(PortContract::Component { .. }) + ) + { + return Err("Selected component port must belong to the reviewed SA participant and have an explicit component contract.".into()); + } + } + CommunicationPortChoice::Create { + id, + alias, + name, + description, + kind, + orientation, + } => { + if id.is_nil() + || working.contains(*id) + || pending.iter().any(|e: &Element| e.id == *id) + { + return Err("New component-port identity must be unused; choose Reuse for an existing port.".into()); + } + pending.push(Element { + id: *id, + alias: alias.clone(), + name: name.clone(), + description: description.clone(), + parent: Some(owner), + body: ElementBody::shared_in( + Layer::Sa, + SharedBody::Port { + contract: PortContract::Component { + kind: *kind, + orientation: *orientation, + }, + legacy: None, + }, + ), + }); + } + } + } + let pair = ( + port_id(&decision.source_port), + port_id(&decision.target_port), + ); + if pair.0 == pair.1 || targets.get(&means.source()) == targets.get(&means.target()) { + return Err("Communication counterparts must have different participant owners; review the participant boundary mapping.".into()); + } + for e in pending { + working.elements.insert(e.id, e.clone()); + plan.commands.push(Command::CreateElement(e)); + } + Ok(pair) + })(); + match result { + Ok(pair) => { + pairs.insert(decision.source, pair); + } + Err(message) => conflict( + plan, + Some(decision.source), + targets.get(&decision.source).copied(), + None, + message, + ), + } + } + pairs +} + +/// Adapt only the transferred endpoint value. Reviewed baseline facts continue +/// to retain the actual participant/port identities on their respective sides. +pub(crate) fn review_endpoint_fact( + project: &Project, + request: &TransitionRequest, + map: &BTreeMap, + source: Id, + target: Id, + fact: &FacetReview, +) -> Result, String> { + let (Some(a), Some(b)) = ( + project.relations.get(&source), + project.relations.get(&target), + ) else { + return Ok(None); + }; + let forward = + a.kind() == RelationKind::CommunicationMean && b.kind() == RelationKind::BoundaryExchange; + let reverse = + a.kind() == RelationKind::BoundaryExchange && b.kind() == RelationKind::CommunicationMean; + if !forward && !reverse { + return Ok(None); + } + let (endpoint, is_source) = match fact { + FacetReview::Relation(RelationReview::Source(_)) => (a.source(), true), + FacetReview::Relation(RelationReview::Target(_)) => (a.target(), false), + _ => return Ok(None), + }; + let mapped = if forward { + let decision = request.communication_endpoints.iter().find(|d| d.source == source) + .ok_or("Taking an OA endpoint requires this exchange's explicit SA component-port decision.")?; + let port = port_id(if is_source { + &decision.source_port + } else { + &decision.target_port + }); + let expected_owner = map.get(&endpoint).ok_or( + "Review the participant counterpart before taking its communication endpoint.", + )?; + let e = project + .elements + .get(&port) + .ok_or("Selected communication endpoint port does not exist.")?; + if e.parent != Some(*expected_owner) + || e.layer() != Layer::Sa + || !matches!( + crate::ports::port_contract(e), + Some(PortContract::Component { .. }) + ) + { + return Err( + "Selected component port disagrees with the reviewed participant counterpart." + .into(), + ); + } + port + } else { + let owner = project + .elements + .get(&endpoint) + .and_then(|e| e.parent) + .ok_or("SA boundary port needs a participant owner.")?; + let participant = *map.get(&owner).ok_or( + "Review the SA port owner's OA participant counterpart before taking this endpoint.", + )?; + if !project.elements.get(&participant).is_some_and(|e| { + e.layer() == Layer::Oa && matches!(e.kind(), Kind::Entity | Kind::Actor) + }) { + return Err("Boundary endpoint owner must map to an OA entity or actor.".into()); + } + participant + }; + let mut result = fact.clone(); + if let FacetReview::Relation(RelationReview::Source(values) | RelationReview::Target(values)) = + &mut result + { + match request.direction.source_layer() { + Layer::Oa => values.oa = Observed::Value(mapped), + Layer::Sa => values.sa = Observed::Value(mapped), + } + } + Ok(Some(result)) +} diff --git a/crates/oa-core/src/validation.rs b/crates/oa-core/src/validation.rs index 8987acc1..0c3d1bc5 100644 --- a/crates/oa-core/src/validation.rs +++ b/crates/oa-core/src/validation.rs @@ -387,6 +387,23 @@ pub fn validate(p: &Project) -> Vec { } if let Some(shared) = e.shared() { match shared { + SharedBody::ExchangeCategory { family, exchanges } => { + for issue in crate::view_options::validate_category( + p, + e.layer(), + e.parent, + *family, + exchanges, + ) { + error( + &mut out, + e.id, + "exchange_category", + issue.field, + issue.message, + ); + } + } SharedBody::DataType { .. } | SharedBody::DataField { .. } | SharedBody::Interface { .. } @@ -601,6 +618,14 @@ pub fn validate(p: &Project) -> Vec { } text(&mut out, d.id, "name", &d.name); text(&mut out, d.id, "description", &d.description); + let option_issues = if d.projection == ProjectionKind::Traceability { + crate::view_options::validate_options_cross_layer(p, &d.view_options) + } else { + crate::view_options::validate_options(p, d.layer, &d.view_options) + }; + for issue in option_issues { + error(&mut out, d.id, "view_options", issue.field, issue.message); + } if let Some(machine) = d.projection.state_machine() && !p .elements @@ -1256,40 +1281,149 @@ pub fn semantic_representation_compatible( pub fn lifeline_matches_endpoint(p: &Project, line: Id, endpoint: Id) -> Option { semantic_representation_compatible(p, represented(p, line)?, endpoint) } -/// Effective semantic direction, including a Reply that reuses its Call exchange. -/// An incomplete reply correlation and a local timer have no inferred endpoints. -pub fn message_endpoints(p: &Project, body: &OccurrenceBody) -> Option<(Id, Id)> { +/// A single shared answer for scenario admission, drawing, and endpoint pickers. +/// Incomplete or ambiguous bindings never choose an arbitrary direction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MessageEndpointResolution { + NotApplicable, + Incomplete, + Resolved { source: Id, target: Id }, + Ambiguous, + Conflict, +} +pub fn resolve_message_endpoints(p: &Project, body: &OccurrenceBody) -> MessageEndpointResolution { + use MessageEndpointResolution as R; let OccurrenceBody::Exchange { exchange: Some(exchange), message_kind, reply_to, + sender, + receiver, .. } = body else { - return None; + return R::NotApplicable; }; if matches!(message_kind, MessageKind::Timer { .. }) { - return None; + return R::NotApplicable; + } + let Some(relation) = p.relations.get(exchange) else { + return R::Incomplete; + }; + let mut candidates = vec![relation.body.endpoints()]; + // A mapped unoriented SA boundary retains the OA communication's possible + // directions. Fixed oriented/legacy exchange behavior remains unchanged. + let communication = matches!( + relation.body, + RelationBody::CommunicationMean { .. } + | RelationBody::BoundaryExchange { + oriented: false, + .. + } + ); + if matches!( + relation.body, + RelationBody::CommunicationMean { + oriented: false, + .. + } | RelationBody::BoundaryExchange { + oriented: false, + .. + } + ) { + candidates.push((relation.target(), relation.source())); + } + if matches!( + relation.body, + RelationBody::BoundaryExchange { + oriented: false, + .. + } + ) { + candidates.retain(|(source, target)| { + match ( + p.elements.get(source).and_then(crate::ports::port_contract), + p.elements.get(target).and_then(crate::ports::port_contract), + ) { + ( + Some(a @ PortContract::Component { .. }), + Some(b @ PortContract::Component { .. }), + ) => { + crate::ports::component_allows(a, true) + && crate::ports::component_allows(b, false) + } + // Legacy direction contracts remain fixed even for an + // unoriented declaration; do not reinterpret old ports. + _ => (*source, *target) == relation.body.endpoints(), + } + }); } - let relation = p.relations.get(exchange)?; if message_kind == &MessageKind::Reply { + let Some(reply_to) = reply_to else { + return R::Incomplete; + }; let Some(SharedBody::Occurrence { body: - OccurrenceBody::Exchange { + call_body @ OccurrenceBody::Exchange { message_kind: MessageKind::Call, exchange: Some(call_exchange), .. }, .. - }) = p.elements.get(&(*reply_to)?).and_then(Element::shared) + }) = p.elements.get(reply_to).and_then(Element::shared) else { - return None; + return R::Incomplete; }; if call_exchange == exchange { - return Some((relation.target(), relation.source())); + let R::Resolved { source, target } = resolve_message_endpoints(p, call_body) else { + return R::Incomplete; + }; + candidates = vec![(target, source)]; + } else if communication { + let R::Resolved { source, target } = resolve_message_endpoints(p, call_body) else { + return R::Incomplete; + }; + let owner = |id| { + p.elements + .get(&id) + .filter(|e| e.kind() == Kind::Port) + .and_then(|e| e.parent) + .unwrap_or(id) + }; + candidates.retain(|(a, b)| { + semantic_representation_compatible(p, owner(*a), owner(target)) == Some(true) + && semantic_representation_compatible(p, owner(*b), owner(source)) == Some(true) + }); } } - Some(relation.body.endpoints()) + if communication { + candidates.retain(|(a, b)| { + sender.is_none_or(|line| lifeline_matches_endpoint(p, line, *a) != Some(false)) + && receiver.is_none_or(|line| lifeline_matches_endpoint(p, line, *b) != Some(false)) + }); + if candidates.is_empty() { + return R::Conflict; + } + if sender.and_then(|line| represented(p, line)).is_none() + || receiver.and_then(|line| represented(p, line)).is_none() + { + return R::Incomplete; + } + if candidates.len() > 1 { + return R::Ambiguous; + } + } + let Some((source, target)) = candidates.first().copied() else { + return R::Conflict; + }; + R::Resolved { source, target } +} +/// Compatibility adapter: only a resolved direction has inferred endpoints. +pub fn message_endpoints(p: &Project, body: &OccurrenceBody) -> Option<(Id, Id)> { + match resolve_message_endpoints(p, body) { + MessageEndpointResolution::Resolved { source, target } => Some((source, target)), + _ => None, + } } /// Only report order guaranteed by a common sequence or an enclosing branch path. /// A call in one alternative does not become an unconditional call after the fragment. @@ -1442,7 +1576,9 @@ fn validate_message(p: &Project, e: &Element, body: &OccurrenceBody, out: &mut V ScenarioViewpoint::Functional => relation.kind() == RelationKind::FunctionalExchange, ScenarioViewpoint::ParticipantInteraction => matches!( relation.kind(), - RelationKind::Interaction | RelationKind::BoundaryExchange + RelationKind::Interaction + | RelationKind::BoundaryExchange + | RelationKind::CommunicationMean ), ScenarioViewpoint::BoundaryInterface => matches!( relation.kind(), @@ -1922,6 +2058,7 @@ fn validate_occurrence(p: &Project, e: &Element, body: &OccurrenceBody, out: &mu RelationKind::Interaction | RelationKind::FunctionalExchange | RelationKind::BoundaryExchange + | RelationKind::CommunicationMean ) => {} _ => error( out, @@ -2136,18 +2273,20 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec) { if let RelationBody::ExchangeAllocation { .. } = r.body { match (p.relations.get(&source), p.relations.get(&target)) { (Some(a), Some(b)) - if a.kind() == RelationKind::BoundaryExchange + if ((a.kind() == RelationKind::BoundaryExchange && matches!( b.kind(), RelationKind::FunctionalExchange | RelationKind::Interaction - ) + )) + || (a.kind() == RelationKind::CommunicationMean + && b.kind() == RelationKind::Interaction)) && p.layer_of(source) == p.layer_of(target) => {} _ => error( out, r.id, "exchange_allocation", "endpoints", - "Exchange allocation relates a boundary exchange to a work exchange in the same layer.", + "Exchange allocation relates communication means to an OA interaction, or a boundary exchange to a work exchange, in the same layer.", ), }; validate_exchange_allocation(p, r, out); @@ -2398,7 +2537,7 @@ pub fn trace_permits(p: &Project, t: &Trace) -> bool { _ => false, }, _ => { - matches!((p.relations.get(&t.oa),p.relations.get(&t.sa)),(Some(a),Some(b))if t.kind==TraceKind::Exchange&&a.kind()==RelationKind::Interaction&&b.kind()==RelationKind::FunctionalExchange) + matches!((p.relations.get(&t.oa),p.relations.get(&t.sa)),(Some(a),Some(b))if t.kind==TraceKind::Exchange&&crate::transitions::exchange_counterparts(a.kind(), b.kind())) } } } @@ -2739,6 +2878,23 @@ fn validate_occurrence_context( OccurrenceBody::Exchange { sender, receiver, .. } => { + match resolve_message_endpoints(p, body) { + MessageEndpointResolution::Conflict => error( + out, + e.id, + "scenario_endpoint", + "sender", + "Sender and receiver must agree with one permitted communication direction and the correlated Call.", + ), + MessageEndpointResolution::Ambiguous => warning( + out, + e.id, + "ambiguous_message_direction", + "sender", + "Bind explicit participant lifelines to resolve the communication direction; shared roles allow both directions.", + ), + _ => {} + } if let Some((source, target)) = message_endpoints(p, body) { check(out, *sender, Some(source), "sender"); check(out, *receiver, Some(target), "receiver"); diff --git a/crates/oa-core/src/validation/coverage.rs b/crates/oa-core/src/validation/coverage.rs index 1e9a2003..c22863ab 100644 --- a/crates/oa-core/src/validation/coverage.rs +++ b/crates/oa-core/src/validation/coverage.rs @@ -138,119 +138,10 @@ fn work_exchange(kind: RelationKind) -> bool { /// Only ordered, owned occurrences contribute. Each identity is visited once, /// including reused containers; this remains bounded even on malformed drafts. fn included_evidence(p: &Project) -> (BTreeSet, BTreeSet) { - let mut pending: Vec<_> = p - .elements - .values() - .filter(|e| { - matches!( - e.kind(), - Kind::Process | Kind::FunctionalChain | Kind::Scenario - ) - }) - .map(|e| e.id) - .collect(); - let mut seen = BTreeSet::new(); - let mut work = BTreeSet::new(); - let mut exchanges = BTreeSet::new(); - while let Some(id) = pending.pop() { - if !seen.insert(id) { - continue; - } - let Some(element) = p.elements.get(&id) else { - continue; - }; - if let Some(steps) = element.steps() { - pending.extend( - steps - .iter() - .filter(|step| { - p.elements.get(step).is_some_and(|child| { - child.kind() == Kind::Occurrence - && child.parent == Some(id) - && child.layer() == element.layer() - }) - }) - .copied(), - ); - } - let Some(SharedBody::Occurrence { body, .. }) = element.shared() else { - continue; - }; - match body { - OccurrenceBody::Action { - action: Some(action), - .. - } => { - if p.elements - .get(action) - .is_some_and(|e| e.kind().is_work() && e.layer() == element.layer()) - { - work.insert(*action); - } - } - OccurrenceBody::Exchange { - exchange: Some(exchange), - .. - } => { - if let Some(relation) = p.relations.get(exchange) - && work_exchange(relation.kind()) - && [relation.source(), relation.target()].iter().all(|id| { - p.elements - .get(id) - .is_some_and(|e| e.kind().is_work() && e.layer() == element.layer()) - }) - { - exchanges.insert(*exchange); - work.extend([relation.source(), relation.target()]); - } - } - OccurrenceBody::Fragment { branches, .. } => { - pending.extend( - branches - .iter() - .filter(|branch| { - p.elements.get(branch).is_some_and(|child| { - child.parent == Some(id) - && child.layer() == element.layer() - && matches!( - child.shared(), - Some(SharedBody::Occurrence { - body: OccurrenceBody::Branch { .. }, - .. - }) - ) - }) - }) - .copied(), - ); - } - OccurrenceBody::SequenceReference { - sequence: Some(sequence), - } => { - if let Some(owner) = element.parent.and_then(|id| p.elements.get(&id)) - && matches!(owner.kind(), Kind::Process | Kind::FunctionalChain) - && p.elements.get(sequence).is_some_and(|target| { - target.kind() == owner.kind() && target.layer() == element.layer() - }) - { - pending.push(*sequence); - } - } - OccurrenceBody::ScenarioUse { - scenario: Some(scenario), - .. - } if p.elements.get(scenario).is_some_and(|target| { - target.kind() == Kind::Scenario && target.layer() == element.layer() - }) => - { - pending.push(*scenario); - } - _ => {} - } - } + let crate::view_options::BehaviorEvidence { work, exchanges } = + crate::view_options::all_behavior_evidence(p); (work, exchanges) } - #[cfg(test)] mod tests { use super::*; diff --git a/crates/oa-core/src/view_options.rs b/crates/oa-core/src/view_options.rs new file mode 100644 index 00000000..481c445e --- /dev/null +++ b/crates/oa-core/src/view_options.rs @@ -0,0 +1,711 @@ +//! Persisted view intent and pure visibility decisions over actual model identities. +//! Build `ViewIndex` once per accepted model revision. Preparing a view performs +//! set unions/reachability once; testing an edge never walks the model. +use crate::{Id, Kind, Layer, OccurrenceBody, Project, RelationKind, SharedBody}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ExchangeFamily { + Work, + Participant, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum EdgeKind { + Relation(RelationKind), + Trace, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum RelationKinds { + #[default] + All, + Only(Vec), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum CategorySelection { + #[default] + All, + Any(Vec), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum ContextSelection { + #[default] + All, + Any(Vec), +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum Coverage { + #[default] + All, + InBehavior, + OutsideBehavior, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ExchangeLabels { + #[default] + Name, + Items, + NameAndItems, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct ViewOptions { + pub hidden_edges: Vec, + pub relation_kinds: RelationKinds, + pub work_categories: CategorySelection, + pub participant_categories: CategorySelection, + pub coverage: Coverage, + pub work_contexts: ContextSelection, + pub exchange_labels: ExchangeLabels, +} + +impl ViewOptions { + pub fn visit_references(&mut self, mut visit: impl FnMut(&mut Id)) { + for id in &mut self.hidden_edges { + visit(id); + } + for selection in [&mut self.work_categories, &mut self.participant_categories] { + if let CategorySelection::Any(ids) = selection { + for id in ids { + visit(id); + } + } + } + if let ContextSelection::Any(ids) = &mut self.work_contexts { + for id in ids { + visit(id); + } + } + } + + /// Removing the last selected identity retains the explicit empty selector. + pub fn retain_references(&mut self, mut keep: impl FnMut(&Id) -> bool) { + self.hidden_edges.retain(&mut keep); + for selection in [&mut self.work_categories, &mut self.participant_categories] { + if let CategorySelection::Any(ids) = selection { + ids.retain(&mut keep); + } + } + if let ContextSelection::Any(ids) = &mut self.work_contexts { + ids.retain(keep); + } + } +} + +/// Stable field names allow core/source adapters to locate the offending option. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OptionError { + pub field: &'static str, + pub reference: Option, + pub message: String, +} + +pub fn exchange_family(kind: RelationKind) -> Option { + match kind { + RelationKind::Interaction | RelationKind::FunctionalExchange => Some(ExchangeFamily::Work), + RelationKind::BoundaryExchange | RelationKind::CommunicationMean => { + Some(ExchangeFamily::Participant) + } + _ => None, + } +} + +fn category_member(p: &Project, layer: Layer, family: ExchangeFamily, id: Id) -> bool { + p.relations.get(&id).is_some_and(|r| { + p.layer_of(id) == Some(layer) + && matches!( + (family, layer, r.kind()), + (ExchangeFamily::Work, Layer::Oa, RelationKind::Interaction) + | ( + ExchangeFamily::Work, + Layer::Sa, + RelationKind::FunctionalExchange + ) + | ( + ExchangeFamily::Participant, + Layer::Oa, + RelationKind::CommunicationMean + ) + | ( + ExchangeFamily::Participant, + _, + RelationKind::BoundaryExchange + ) + ) + }) +} + +fn context_kind(kind: Kind) -> bool { + matches!(kind, Kind::Process | Kind::FunctionalChain | Kind::Scenario) +} + +fn edge_layers(p: &Project, id: Id) -> Vec { + if p.relations.contains_key(&id) { + p.layer_of(id).into_iter().collect() + } else if p.traces.get(&id).is_some_and(|trace| { + p.layer_of(trace.oa) == Some(Layer::Oa) && p.layer_of(trace.sa) == Some(Layer::Sa) + }) { + vec![Layer::Oa, Layer::Sa] + } else { + vec![] + } +} + +fn error(field: &'static str, reference: Option, message: &str) -> OptionError { + OptionError { + field, + reference, + message: message.into(), + } +} + +fn distinct_ids(field: &'static str, ids: &[Id], out: &mut Vec) { + let mut seen = BTreeSet::new(); + for id in ids { + if !seen.insert(*id) { + out.push(error( + field, + Some(*id), + "Each identity must occur only once.", + )); + } + } +} + +pub fn validate_category( + p: &Project, + layer: Layer, + parent: Option, + family: ExchangeFamily, + exchanges: &[Id], +) -> Vec { + let mut out = vec![]; + if parent.is_some() { + out.push(error( + "parent", + parent, + "Exchange categories belong at the layer root.", + )); + } + distinct_ids("exchanges", exchanges, &mut out); + for id in exchanges { + if !category_member(p, layer, family, *id) { + out.push(error( + "exchanges", + Some(*id), + "Choose an existing exchange of this category's family and layer.", + )); + } + } + out +} + +pub fn validate_options(p: &Project, layer: Layer, options: &ViewOptions) -> Vec { + validate_options_in(p, Some(layer), options) +} + +/// Traceability views admit references from either architecture layer. +pub fn validate_options_cross_layer(p: &Project, options: &ViewOptions) -> Vec { + validate_options_in(p, None, options) +} + +fn validate_options_in( + p: &Project, + layer: Option, + options: &ViewOptions, +) -> Vec { + let mut out = vec![]; + distinct_ids("hidden_edges", &options.hidden_edges, &mut out); + for id in &options.hidden_edges { + let layers = edge_layers(p, *id); + if layers.is_empty() || layer.is_some_and(|layer| !layers.contains(&layer)) { + out.push(error( + "hidden_edges", + Some(*id), + "Choose an existing relationship or applicable trace in this view's layer.", + )); + } + } + if let RelationKinds::Only(kinds) = &options.relation_kinds { + for (index, kind) in kinds.iter().enumerate() { + if kinds[..index].contains(kind) { + out.push(error( + "relation_kinds", + None, + "Each relationship-line kind must occur only once.", + )); + } + } + } + for (field, selection, family) in [ + ( + "work_categories", + &options.work_categories, + ExchangeFamily::Work, + ), + ( + "participant_categories", + &options.participant_categories, + ExchangeFamily::Participant, + ), + ] { + if let CategorySelection::Any(ids) = selection { + distinct_ids(field, ids, &mut out); + for id in ids { + if !p.elements.get(id).is_some_and(|e| { + layer.is_none_or(|layer| e.layer() == layer) + && matches!(e.shared(), Some(SharedBody::ExchangeCategory { family: f, .. }) if *f == family) + }) { + out.push(error(field, Some(*id), "Choose a category of this selector's family and layer.")); + } + } + } + } + if let ContextSelection::Any(ids) = &options.work_contexts { + distinct_ids("work_contexts", ids, &mut out); + for id in ids { + if !p.elements.get(id).is_some_and(|e| { + layer.is_none_or(|layer| e.layer() == layer) && context_kind(e.kind()) + }) { + out.push(error( + "work_contexts", + Some(*id), + "Choose a Process, Functional chain or Scenario in this view's layer.", + )); + } + } + } + out +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BehaviorEvidence { + pub work: BTreeSet, + pub exchanges: BTreeSet, +} + +/// Explicit inclusion/reuse graph, independent of semantic-parent descendants. +#[derive(Debug, Clone, Default)] +struct BehaviorIndex { + roots: BTreeMap, + next: BTreeMap>, + facts: BTreeMap, +} + +impl BehaviorIndex { + fn new(p: &Project) -> Self { + let mut index = Self::default(); + for element in p.elements.values() { + let id = element.id; + if context_kind(element.kind()) { + index.roots.insert(id, element.layer()); + } + let next = index.next.entry(id).or_default(); + if let Some(steps) = element.steps() { + next.extend( + steps + .iter() + .filter(|step| { + p.elements.get(step).is_some_and(|child| { + child.kind() == Kind::Occurrence + && child.parent == Some(id) + && child.layer() == element.layer() + }) + }) + .copied(), + ); + } + let Some(SharedBody::Occurrence { body, .. }) = element.shared() else { + continue; + }; + match body { + OccurrenceBody::Action { + action: Some(work), .. + } => { + if p.elements + .get(work) + .is_some_and(|e| e.kind().is_work() && e.layer() == element.layer()) + { + index.facts.entry(id).or_default().work.insert(*work); + } + } + OccurrenceBody::Exchange { + exchange: Some(exchange), + .. + } => { + if let Some(r) = p.relations.get(exchange) + && exchange_family(r.kind()) == Some(ExchangeFamily::Work) + && [r.source(), r.target()].iter().all(|id| { + p.elements + .get(id) + .is_some_and(|e| e.kind().is_work() && e.layer() == element.layer()) + }) + { + let facts = index.facts.entry(id).or_default(); + facts.exchanges.insert(*exchange); + facts.work.extend([r.source(), r.target()]); + } + } + OccurrenceBody::Fragment { branches, .. } => { + next.extend( + branches + .iter() + .filter(|branch| { + p.elements.get(branch).is_some_and(|child| { + child.parent == Some(id) + && child.layer() == element.layer() + && matches!( + child.shared(), + Some(SharedBody::Occurrence { + body: OccurrenceBody::Branch { .. }, + .. + }) + ) + }) + }) + .copied(), + ); + } + OccurrenceBody::SequenceReference { + sequence: Some(sequence), + } => { + if let Some(owner) = element.parent.and_then(|id| p.elements.get(&id)) + && matches!(owner.kind(), Kind::Process | Kind::FunctionalChain) + && p.elements.get(sequence).is_some_and(|target| { + target.kind() == owner.kind() && target.layer() == element.layer() + }) + { + next.push(*sequence); + } + } + OccurrenceBody::ScenarioUse { + scenario: Some(scenario), + .. + } if p.elements.get(scenario).is_some_and(|target| { + target.kind() == Kind::Scenario && target.layer() == element.layer() + }) => + { + next.push(*scenario); + } + _ => {} + } + } + index + } + + fn evidence(&self, roots: impl IntoIterator) -> BehaviorEvidence { + let mut pending: Vec<_> = roots + .into_iter() + .filter(|id| self.roots.contains_key(id)) + .collect(); + let mut seen = BTreeSet::new(); + let mut result = BehaviorEvidence::default(); + while let Some(id) = pending.pop() { + if !seen.insert(id) { + continue; + } + if let Some(next) = self.next.get(&id) { + pending.extend(next); + } + if let Some(facts) = self.facts.get(&id) { + result.work.extend(&facts.work); + result.exchanges.extend(&facts.exchanges); + } + } + result + } +} + +pub fn behavior_evidence(p: &Project, roots: impl IntoIterator) -> BehaviorEvidence { + BehaviorIndex::new(p).evidence(roots) +} + +pub fn all_behavior_evidence(p: &Project) -> BehaviorEvidence { + let index = BehaviorIndex::new(p); + index.evidence(index.roots.keys().copied()) +} + +#[derive(Debug, Clone)] +struct EdgeFact { + kind: EdgeKind, + family: Option, + layers: Vec, +} + +#[derive(Debug, Clone)] +struct CategoryFact { + layer: Layer, + family: ExchangeFamily, + exchanges: BTreeSet, +} + +#[derive(Debug, Clone)] +pub struct ViewIndex { + edges: BTreeMap, + categories: BTreeMap, + behavior: BehaviorIndex, + global: BehaviorEvidence, +} + +impl ViewIndex { + pub fn new(p: &Project) -> Self { + let behavior = BehaviorIndex::new(p); + let global = behavior.evidence(behavior.roots.keys().copied()); + let mut edges: BTreeMap<_, _> = p + .relations + .values() + .map(|r| { + ( + r.id, + EdgeFact { + kind: EdgeKind::Relation(r.kind()), + family: exchange_family(r.kind()), + layers: edge_layers(p, r.id), + }, + ) + }) + .collect(); + edges.extend(p.traces.keys().map(|id| { + ( + *id, + EdgeFact { + kind: EdgeKind::Trace, + family: None, + layers: edge_layers(p, *id), + }, + ) + })); + let categories = p + .elements + .values() + .filter_map(|e| { + let SharedBody::ExchangeCategory { family, exchanges } = e.shared()? else { + return None; + }; + Some(( + e.id, + CategoryFact { + layer: e.layer(), + family: *family, + exchanges: exchanges + .iter() + .filter(|id| category_member(p, e.layer(), *family, **id)) + .copied() + .collect(), + }, + )) + }) + .collect(); + Self { + edges, + categories, + behavior, + global, + } + } + + pub fn all_evidence(&self) -> &BehaviorEvidence { + &self.global + } + + /// Options must pass `validate_options` before model admission. Malformed + /// selectors are nevertheless fail-closed here, useful for draft previews. + pub fn prepare(&self, layer: Layer, options: &ViewOptions) -> PreparedFilter<'_> { + self.prepare_in(layer, options, false) + } + + fn prepare_in( + &self, + layer: Layer, + options: &ViewOptions, + cross_layer: bool, + ) -> PreparedFilter<'_> { + let category_union = |selection: &CategorySelection, family| match selection { + CategorySelection::All => None, + CategorySelection::Any(ids) => Some( + ids.iter() + .filter_map(|id| self.categories.get(id)) + .filter(|c| (cross_layer || c.layer == layer) && c.family == family) + .flat_map(|c| c.exchanges.iter().copied()) + .collect(), + ), + }; + let contexts = match &options.work_contexts { + ContextSelection::All => None, + ContextSelection::Any(ids) => Some( + self.behavior + .evidence( + ids.iter() + .filter(|id| { + self.behavior + .roots + .get(id) + .is_some_and(|candidate| cross_layer || *candidate == layer) + }) + .copied(), + ) + .exchanges, + ), + }; + PreparedFilter { + index: self, + layer, + cross_layer, + hidden: options.hidden_edges.iter().copied().collect(), + kinds: options.relation_kinds.clone(), + coverage: options.coverage, + work_categories: category_union(&options.work_categories, ExchangeFamily::Work), + participant_categories: category_union( + &options.participant_categories, + ExchangeFamily::Participant, + ), + contexts, + } + } + + /// Traceability diagrams admit edges, categories and contexts in both layers. + pub fn prepare_cross_layer(&self, layer: Layer, options: &ViewOptions) -> PreparedFilter<'_> { + self.prepare_in(layer, options, true) + } +} + +/// Projection reports why an actual edge cannot have a drawable endpoint pair. +/// Filtering does not perform anchoring, folding, layout or placement insertion. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum EdgeAvailability { + Visible, + OutsideProjection, + EndpointNotShown { source: bool, target: bool }, + FoldedInternal, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum OmissionReason { + ExplicitHide, + RelationKind, + Category(ExchangeFamily), + Coverage, + WorkContext, + OutsideProjection, + EndpointNotShown { source: bool, target: bool }, + FoldedInternal, + WrongLayer, + UnknownEdge, +} + +pub struct PreparedFilter<'a> { + index: &'a ViewIndex, + layer: Layer, + cross_layer: bool, + hidden: BTreeSet, + kinds: RelationKinds, + coverage: Coverage, + work_categories: Option>, + participant_categories: Option>, + contexts: Option>, +} + +impl PreparedFilter<'_> { + pub fn includes(&self, edge: Id) -> bool { + self.reasons(edge, EdgeAvailability::Visible).is_empty() + } + + pub fn reasons(&self, edge: Id, availability: EdgeAvailability) -> Vec { + let mut out = vec![]; + if self.hidden.contains(&edge) { + out.push(OmissionReason::ExplicitHide); + } + match availability { + EdgeAvailability::Visible => {} + EdgeAvailability::OutsideProjection => out.push(OmissionReason::OutsideProjection), + EdgeAvailability::EndpointNotShown { source, target } => { + out.push(OmissionReason::EndpointNotShown { source, target }) + } + EdgeAvailability::FoldedInternal => out.push(OmissionReason::FoldedInternal), + } + let Some(fact) = self.index.edges.get(&edge) else { + out.push(OmissionReason::UnknownEdge); + return out; + }; + if !fact.layers.contains(&self.layer) && !(self.cross_layer && !fact.layers.is_empty()) { + out.push(OmissionReason::WrongLayer); + } + if let RelationKinds::Only(kinds) = &self.kinds + && !kinds.contains(&fact.kind) + { + out.push(OmissionReason::RelationKind); + } + if let Some(family) = fact.family { + let categories = match family { + ExchangeFamily::Work => &self.work_categories, + ExchangeFamily::Participant => &self.participant_categories, + }; + if categories.as_ref().is_some_and(|ids| !ids.contains(&edge)) { + out.push(OmissionReason::Category(family)); + } + if family == ExchangeFamily::Work { + let covered = self.index.global.exchanges.contains(&edge); + if matches!(self.coverage, Coverage::InBehavior) && !covered + || matches!(self.coverage, Coverage::OutsideBehavior) && covered + { + out.push(OmissionReason::Coverage); + } + if self + .contexts + .as_ref() + .is_some_and(|ids| !ids.contains(&edge)) + { + out.push(OmissionReason::WorkContext); + } + } + } + out + } +} + +impl ExchangeLabels { + pub fn label(self, p: &Project, edge: Id) -> String { + let name = p.named(edge); + let Some(relation) = p + .relations + .get(&edge) + .filter(|r| exchange_family(r.kind()).is_some()) + else { + return name.into(); + }; + if self == Self::Name { + return name.into(); + } + let items = if relation.body.items().is_empty() { + "No items".into() + } else { + relation + .body + .items() + .iter() + .map(|id| p.named(*id)) + .collect::>() + .join(", ") + }; + if self == Self::Items { + items + } else { + format!("{name} · {items}") + } + } +} + +#[cfg(test)] +#[path = "view_options_tests.rs"] +mod tests; diff --git a/crates/oa-core/src/view_options_tests.rs b/crates/oa-core/src/view_options_tests.rs new file mode 100644 index 00000000..677b505c --- /dev/null +++ b/crates/oa-core/src/view_options_tests.rs @@ -0,0 +1,620 @@ +use super::*; +use crate::{FragmentOperator, MessageKind, RelationBody, Trace, TraceKind}; + +fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id { + p.add_element(kind, layer, name, None).unwrap() +} + +fn occurrence(p: &mut Project, owner: Id, body: OccurrenceBody) -> Id { + let id = p + .add_element( + Kind::Occurrence, + p.elements[&owner].layer(), + "Contribution", + Some(owner), + ) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { + body, + precondition: String::new(), + postcondition: String::new(), + }; + if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut() { + steps.push(id); + } + id +} + +fn exchange(id: Id) -> OccurrenceBody { + OccurrenceBody::Exchange { + exchange: Some(id), + sender: None, + receiver: None, + message_kind: MessageKind::Asynchronous, + reply_to: None, + values: vec![], + } +} + +fn category(p: &mut Project, layer: Layer, family: ExchangeFamily, exchanges: Vec) -> Id { + let id = add(p, Kind::ExchangeCategory, layer, "Concern"); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = + SharedBody::ExchangeCategory { family, exchanges }; + id +} + +struct Model { + p: Project, + work: [Id; 4], + means: [Id; 2], + categories: [Id; 3], + contexts: [Id; 2], + allocation: Id, + trace: Id, +} + +fn model() -> Model { + let mut p = Project::empty(); + let a = add(&mut p, Kind::Activity, Layer::Oa, "Request"); + let b = add(&mut p, Kind::Activity, Layer::Oa, "Provide"); + let c = add(&mut p, Kind::Activity, Layer::Oa, "Monitor"); + let x = add(&mut p, Kind::Entity, Layer::Oa, "Requester"); + let y = add(&mut p, Kind::Entity, Layer::Oa, "Provider"); + let z = add(&mut p, Kind::Entity, Layer::Oa, "Observer"); + let allocation = p.relate(RelationKind::Allocation, "Responsible", x, a); + p.relate(RelationKind::Allocation, "Responsible", y, b); + p.relate(RelationKind::Allocation, "Responsible", z, c); + let work = [ + p.relate(RelationKind::Interaction, "Request data", a, b), + p.relate(RelationKind::Interaction, "Report", b, c), + p.relate(RelationKind::Interaction, "Feedback", c, a), + p.relate(RelationKind::Interaction, "Direct monitor", a, c), + ]; + let means = [ + p.relate(RelationKind::CommunicationMean, "Service channel", x, y), + p.relate(RelationKind::CommunicationMean, "Monitoring channel", y, z), + ]; + let contexts = [ + add(&mut p, Kind::Process, Layer::Oa, "Service path"), + add(&mut p, Kind::Process, Layer::Oa, "Feedback path"), + ]; + for edge in &work[..2] { + occurrence(&mut p, contexts[0], exchange(*edge)); + } + for edge in &work[1..3] { + occurrence(&mut p, contexts[1], exchange(*edge)); + } + let categories = [ + category(&mut p, Layer::Oa, ExchangeFamily::Work, work[..2].to_vec()), + category(&mut p, Layer::Oa, ExchangeFamily::Work, work[1..3].to_vec()), + category( + &mut p, + Layer::Oa, + ExchangeFamily::Participant, + vec![means[0]], + ), + ]; + let function = add(&mut p, Kind::Function, Layer::Sa, "System request"); + let trace = Id::new_v4(); + p.traces.insert( + trace, + Trace { + id: trace, + alias: "request_realization".into(), + name: "Request realization".into(), + description: String::new(), + oa: a, + sa: function, + kind: TraceKind::Refinement, + baseline: None, + }, + ); + Model { + p, + work, + means, + categories, + contexts, + allocation, + trace, + } +} + +fn accepted(filter: &PreparedFilter<'_>, ids: &[Id]) -> Vec { + ids.iter() + .filter(|id| filter.includes(**id)) + .copied() + .collect() +} + +#[test] +fn traceability_defaults_preserve_both_layers_and_explicit_selectors_still_apply() { + let mut m = model(); + let a = add(&mut m.p, Kind::Function, Layer::Sa, "Request in system"); + let b = add(&mut m.p, Kind::Function, Layer::Sa, "Provide in system"); + let sa_exchange = + m.p.relate(RelationKind::FunctionalExchange, "System data", a, b); + let sa_category = category(&mut m.p, Layer::Sa, ExchangeFamily::Work, vec![sa_exchange]); + let sa_context = add( + &mut m.p, + Kind::FunctionalChain, + Layer::Sa, + "System service path", + ); + occurrence(&mut m.p, sa_context, exchange(sa_exchange)); + let index = ViewIndex::new(&m.p); + let defaults = ViewOptions::default(); + assert!(!index.prepare(Layer::Oa, &defaults).includes(sa_exchange)); + let cross_layer = index.prepare_cross_layer(Layer::Oa, &defaults); + assert!(cross_layer.includes(sa_exchange)); + assert!(cross_layer.includes(m.work[0])); + assert!(cross_layer.includes(m.trace)); + let selected = ViewOptions { + work_categories: CategorySelection::Any(vec![m.categories[0]]), + ..defaults + }; + let cross_layer = index.prepare_cross_layer(Layer::Oa, &selected); + assert!(cross_layer.includes(m.work[0])); + assert_eq!( + cross_layer.reasons(sa_exchange, EdgeAvailability::Visible), + vec![OmissionReason::Category(ExchangeFamily::Work)] + ); + let none = ViewOptions { + relation_kinds: RelationKinds::Only(vec![]), + ..ViewOptions::default() + }; + assert!( + !index + .prepare_cross_layer(Layer::Oa, &none) + .includes(sa_exchange) + ); + let mixed = ViewOptions { + work_categories: CategorySelection::Any(vec![m.categories[0], sa_category]), + work_contexts: ContextSelection::Any(vec![m.contexts[0], sa_context]), + hidden_edges: vec![sa_exchange], + ..ViewOptions::default() + }; + assert!(validate_options_cross_layer(&m.p, &mixed).is_empty()); + assert!(!validate_options(&m.p, Layer::Oa, &mixed).is_empty()); + let cross_layer = index.prepare_cross_layer(Layer::Oa, &mixed); + assert!(cross_layer.includes(m.work[0])); + assert_eq!( + cross_layer.reasons(sa_exchange, EdgeAvailability::Visible), + vec![OmissionReason::ExplicitHide] + ); +} + +#[test] +fn separate_category_families_obey_all_any_empty_and_union_truth_table() { + let m = model(); + let index = ViewIndex::new(&m.p); + let work_cases = [ + (CategorySelection::All, m.work.to_vec()), + (CategorySelection::Any(vec![]), vec![]), + ( + CategorySelection::Any(vec![m.categories[0]]), + m.work[..2].to_vec(), + ), + ( + CategorySelection::Any(m.categories[..2].to_vec()), + m.work[..3].to_vec(), + ), + ]; + let participant_cases = [ + (CategorySelection::All, m.means.to_vec()), + (CategorySelection::Any(vec![]), vec![]), + ( + CategorySelection::Any(vec![m.categories[2]]), + vec![m.means[0]], + ), + ]; + for (work_categories, expected_work) in &work_cases { + for (participant_categories, expected_means) in &participant_cases { + let options = ViewOptions { + work_categories: work_categories.clone(), + participant_categories: participant_categories.clone(), + ..ViewOptions::default() + }; + assert!(validate_options(&m.p, Layer::Oa, &options).is_empty()); + let filter = index.prepare(Layer::Oa, &options); + assert_eq!(&accepted(&filter, &m.work), expected_work); + assert_eq!(&accepted(&filter, &m.means), expected_means); + assert!(filter.includes(m.allocation)); + assert!(filter.includes(m.trace)); + } + } +} + +#[test] +fn global_behavior_coverage_and_chosen_context_are_independent_intersecting_predicates() { + let m = model(); + let index = ViewIndex::new(&m.p); + let coverage_cases = [ + (Coverage::All, m.work.to_vec()), + (Coverage::InBehavior, m.work[..3].to_vec()), + (Coverage::OutsideBehavior, vec![m.work[3]]), + ]; + let context_cases = [ + (ContextSelection::All, m.work.to_vec()), + (ContextSelection::Any(vec![]), vec![]), + ( + ContextSelection::Any(vec![m.contexts[0]]), + m.work[..2].to_vec(), + ), + ( + ContextSelection::Any(vec![m.contexts[1]]), + m.work[1..3].to_vec(), + ), + ( + ContextSelection::Any(m.contexts.to_vec()), + m.work[..3].to_vec(), + ), + ]; + for (coverage, global) in &coverage_cases { + for (work_contexts, chosen) in &context_cases { + let expected: Vec<_> = global + .iter() + .filter(|id| chosen.contains(id)) + .copied() + .collect(); + let options = ViewOptions { + coverage: *coverage, + work_contexts: work_contexts.clone(), + ..ViewOptions::default() + }; + let filter = index.prepare(Layer::Oa, &options); + assert_eq!(accepted(&filter, &m.work), expected); + assert_eq!(accepted(&filter, &m.means), m.means); + assert!(filter.includes(m.allocation)); + } + } +} + +#[test] +fn no_lines_and_explicit_trace_selection_include_actual_trace_identities() { + let m = model(); + let index = ViewIndex::new(&m.p); + let all: Vec<_> = m + .work + .into_iter() + .chain(m.means) + .chain([m.allocation, m.trace]) + .collect(); + for (relation_kinds, expected) in [ + (RelationKinds::All, all.clone()), + (RelationKinds::Only(vec![]), vec![]), + (RelationKinds::Only(vec![EdgeKind::Trace]), vec![m.trace]), + ( + RelationKinds::Only(vec![ + EdgeKind::Relation(RelationKind::Interaction), + EdgeKind::Trace, + ]), + m.work.into_iter().chain([m.trace]).collect(), + ), + ] { + let options = ViewOptions { + relation_kinds, + ..ViewOptions::default() + }; + assert_eq!( + accepted(&index.prepare(Layer::Oa, &options), &all), + expected + ); + } +} + +#[test] +fn hidden_identity_survives_label_and_category_changes_without_mutating_the_model() { + let mut m = model(); + let item = add(&mut m.p, Kind::ExchangeItem, Layer::Oa, "Request payload"); + if let RelationBody::Interaction { items, .. } = + &mut m.p.relations.get_mut(&m.work[0]).unwrap().body + { + items.push(item); + } + let before = m.p.clone(); + let index = ViewIndex::new(&m.p); + let mut options = ViewOptions { + hidden_edges: vec![m.work[0]], + ..ViewOptions::default() + }; + for labels in [ + ExchangeLabels::Name, + ExchangeLabels::Items, + ExchangeLabels::NameAndItems, + ] { + options.exchange_labels = labels; + for categories in [ + CategorySelection::All, + CategorySelection::Any(vec![m.categories[0]]), + CategorySelection::Any(vec![]), + ] { + options.work_categories = categories; + let filter = index.prepare(Layer::Oa, &options); + assert!(!filter.includes(m.work[0])); + assert!( + filter + .reasons(m.work[0], EdgeAvailability::Visible) + .contains(&OmissionReason::ExplicitHide) + ); + } + } + assert_eq!( + ExchangeLabels::Items.label(&m.p, m.work[0]), + "Request payload" + ); + assert_eq!( + ExchangeLabels::NameAndItems.label(&m.p, m.work[0]), + "Request data · Request payload" + ); + assert_eq!(ExchangeLabels::Items.label(&m.p, m.work[1]), "No items"); + assert_eq!( + ExchangeLabels::Items.label(&m.p, m.trace), + "Request realization" + ); + assert_eq!(m.p, before); + assert_eq!(options.hidden_edges, vec![m.work[0]]); +} + +#[test] +fn omission_reasons_distinguish_missing_placements_folded_edges_and_explicit_hides() { + let m = model(); + let index = ViewIndex::new(&m.p); + let options = ViewOptions::default(); + let filter = index.prepare(Layer::Oa, &options); + assert_eq!( + filter.reasons( + m.work[0], + EdgeAvailability::EndpointNotShown { + source: false, + target: true + } + ), + vec![OmissionReason::EndpointNotShown { + source: false, + target: true + }] + ); + assert_eq!( + filter.reasons(m.work[0], EdgeAvailability::FoldedInternal), + vec![OmissionReason::FoldedInternal] + ); + assert_eq!( + filter.reasons(m.work[0], EdgeAvailability::OutsideProjection), + vec![OmissionReason::OutsideProjection] + ); + let options = ViewOptions { + hidden_edges: vec![m.work[0]], + work_categories: CategorySelection::Any(vec![]), + ..options + }; + let reasons = index.prepare(Layer::Oa, &options).reasons( + m.work[0], + EdgeAvailability::EndpointNotShown { + source: true, + target: true, + }, + ); + assert!(reasons.contains(&OmissionReason::ExplicitHide)); + assert!(reasons.contains(&OmissionReason::Category(ExchangeFamily::Work))); + assert!(reasons.contains(&OmissionReason::EndpointNotShown { + source: true, + target: true + })); +} + +#[test] +fn wrong_family_layer_and_unknown_references_reject_and_draft_filters_fail_closed() { + let m = model(); + let index = ViewIndex::new(&m.p); + for wrong in [m.categories[2], m.work[0], Id::new_v4()] { + let options = ViewOptions { + work_categories: CategorySelection::Any(vec![wrong]), + ..ViewOptions::default() + }; + assert!( + validate_options(&m.p, Layer::Oa, &options) + .iter() + .any(|e| e.field == "work_categories") + ); + assert!(accepted(&index.prepare(Layer::Oa, &options), &m.work).is_empty()); + } + let options = ViewOptions { + work_categories: CategorySelection::Any(vec![m.categories[0]]), + work_contexts: ContextSelection::Any(vec![m.contexts[0]]), + hidden_edges: vec![m.work[0]], + ..ViewOptions::default() + }; + let errors = validate_options(&m.p, Layer::Sa, &options); + for field in ["work_categories", "work_contexts", "hidden_edges"] { + assert!(errors.iter().any(|e| e.field == field)); + } + assert!(validate_category(&m.p, Layer::Oa, None, ExchangeFamily::Work, &m.work).is_empty()); + assert!( + validate_category(&m.p, Layer::Oa, None, ExchangeFamily::Participant, &m.means).is_empty() + ); + for (family, members) in [ + (ExchangeFamily::Work, m.means.to_vec()), + (ExchangeFamily::Participant, m.work.to_vec()), + (ExchangeFamily::Work, vec![m.work[0], m.work[0]]), + ] { + assert!(!validate_category(&m.p, Layer::Oa, None, family, &members).is_empty()); + } + assert!( + !validate_category( + &m.p, + Layer::Oa, + Some(m.contexts[0]), + ExchangeFamily::Work, + &[] + ) + .is_empty() + ); +} + +#[test] +fn persisted_empty_selectors_and_lifecycle_never_widen_to_all() { + let m = model(); + let mut options = ViewOptions { + hidden_edges: vec![m.work[0]], + relation_kinds: RelationKinds::Only(vec![]), + work_categories: CategorySelection::Any(vec![m.categories[0]]), + participant_categories: CategorySelection::Any(vec![m.categories[2]]), + work_contexts: ContextSelection::Any(vec![m.contexts[0]]), + coverage: Coverage::OutsideBehavior, + exchange_labels: ExchangeLabels::Items, + }; + let replacements: BTreeMap<_, _> = [m.work[0], m.categories[0], m.categories[2], m.contexts[0]] + .into_iter() + .map(|id| (id, Id::new_v4())) + .collect(); + options.visit_references(|id| *id = replacements[id]); + assert_eq!(options.hidden_edges, vec![replacements[&m.work[0]]]); + assert_eq!( + options.work_categories, + CategorySelection::Any(vec![replacements[&m.categories[0]]]) + ); + assert_eq!( + options.participant_categories, + CategorySelection::Any(vec![replacements[&m.categories[2]]]) + ); + assert_eq!( + options.work_contexts, + ContextSelection::Any(vec![replacements[&m.contexts[0]]]) + ); + options.retain_references(|_| false); + assert_eq!(options.work_categories, CategorySelection::Any(vec![])); + assert_eq!( + options.participant_categories, + CategorySelection::Any(vec![]) + ); + assert_eq!(options.work_contexts, ContextSelection::Any(vec![])); + assert_eq!(options.relation_kinds, RelationKinds::Only(vec![])); + assert_eq!(options.coverage, Coverage::OutsideBehavior); + assert_eq!(options.exchange_labels, ExchangeLabels::Items); + assert_eq!( + serde_json::from_value::(serde_json::to_value(&options).unwrap()).unwrap(), + options + ); + assert_ne!(options, ViewOptions::default()); + assert_eq!( + serde_json::from_str::("{}").unwrap(), + ViewOptions::default() + ); + assert!(serde_json::from_str::(r#"{"future_filter":true}"#).is_err()); + assert!(serde_json::from_str::(r#"{"coverage":"Maybe"}"#).is_err()); + let duplicate = ViewOptions { + relation_kinds: RelationKinds::Only(vec![EdgeKind::Trace, EdgeKind::Trace]), + hidden_edges: vec![m.trace, m.trace], + ..ViewOptions::default() + }; + let errors = validate_options(&m.p, Layer::Oa, &duplicate); + assert!(errors.iter().any(|e| e.field == "relation_kinds")); + assert!(errors.iter().any(|e| e.field == "hidden_edges")); +} + +#[test] +fn reusable_sequences_and_nested_scenarios_use_listed_contributions_in_both_layers() { + for (layer, work_kind, sequence_kind, exchange_kind) in [ + ( + Layer::Oa, + Kind::Activity, + Kind::Process, + RelationKind::Interaction, + ), + ( + Layer::Sa, + Kind::Function, + Kind::FunctionalChain, + RelationKind::FunctionalExchange, + ), + ] { + let mut p = Project::empty(); + let a = add(&mut p, work_kind, layer, "Request"); + let b = add(&mut p, work_kind, layer, "Provide"); + let used = p.relate(exchange_kind, "Included", a, b); + let unlisted = p.relate(exchange_kind, "Unlisted", b, a); + let inner = add(&mut p, sequence_kind, layer, "Reusable sequence"); + let outer = add(&mut p, sequence_kind, layer, "Chosen sequence"); + occurrence(&mut p, inner, exchange(used)); + occurrence( + &mut p, + outer, + OccurrenceBody::SequenceReference { + sequence: Some(inner), + }, + ); + let unused = occurrence(&mut p, inner, exchange(unlisted)); + p.elements + .get_mut(&inner) + .unwrap() + .steps_mut() + .unwrap() + .retain(|id| *id != unused); + assert_eq!( + behavior_evidence(&p, [outer]).exchanges, + BTreeSet::from([used]) + ); + assert!(behavior_evidence(&p, [unused]).exchanges.is_empty()); + let scenario = add(&mut p, Kind::Scenario, layer, "Conversation"); + let fragment = occurrence( + &mut p, + scenario, + OccurrenceBody::Fragment { + operator: FragmentOperator::Optional, + branches: vec![], + }, + ); + let branch = occurrence( + &mut p, + fragment, + OccurrenceBody::Branch { + guard: "requested".into(), + occurrences: vec![], + }, + ); + if let Some(SharedBody::Occurrence { + body: OccurrenceBody::Fragment { branches, .. }, + .. + }) = p.elements.get_mut(&fragment).unwrap().shared_mut() + { + branches.push(branch); + } + occurrence(&mut p, branch, exchange(unlisted)); + let reuse = add(&mut p, Kind::Scenario, layer, "Chosen conversation"); + occurrence( + &mut p, + reuse, + OccurrenceBody::ScenarioUse { + scenario: Some(scenario), + bindings: vec![], + }, + ); + assert_eq!( + behavior_evidence(&p, [reuse]).exchanges, + BTreeSet::from([unlisted]) + ); + assert_eq!( + all_behavior_evidence(&p).exchanges, + BTreeSet::from([used, unlisted]) + ); + assert_eq!(behavior_evidence(&p, [outer]).work, BTreeSet::from([a, b])); + p.elements + .get_mut(&scenario) + .unwrap() + .steps_mut() + .unwrap() + .clear(); + assert!(behavior_evidence(&p, [reuse]).exchanges.is_empty()); + // A malformed reuse cycle must terminate without inventing an occurrence. + occurrence( + &mut p, + scenario, + OccurrenceBody::ScenarioUse { + scenario: Some(reuse), + bindings: vec![], + }, + ); + assert!(behavior_evidence(&p, [reuse]).exchanges.is_empty()); + } +} diff --git a/crates/oa-core/tests/communication_boundary_scenarios.rs b/crates/oa-core/tests/communication_boundary_scenarios.rs new file mode 100644 index 00000000..c91393e0 --- /dev/null +++ b/crates/oa-core/tests/communication_boundary_scenarios.rs @@ -0,0 +1,248 @@ +//! Independent oracle for the OA communication / SA boundary direction seam. +use oa_core::{ + transitions::*, + validation::{self, MessageEndpointResolution as Resolution}, + *, +}; + +fn line(p: &mut Project, scenario: Id, owner: Id) -> Id { + let id = p + .add_element(Kind::Lifeline, Layer::Sa, "Line", Some(scenario)) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(owner), + }; + id +} +fn message( + exchange: Id, + sender: Id, + receiver: Id, + kind: MessageKind, + reply: Option, +) -> OccurrenceBody { + OccurrenceBody::Exchange { + exchange: Some(exchange), + sender: Some(sender), + receiver: Some(receiver), + message_kind: kind, + reply_to: reply, + values: vec![], + } +} +fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id { + let id = p + .add_element(Kind::Occurrence, Layer::Sa, "Message", Some(scenario)) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { + body, + precondition: String::new(), + postcondition: String::new(), + }; + let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap(); + if !steps.contains(&id) { + steps.push(id); + } + id +} +fn boundary_fixture() -> (Project, Id, Id, Id, Id) { + let mut p = Project::empty(); + let oa = p + .add_element(Kind::Entity, Layer::Oa, "Team", None) + .unwrap(); + let ob = p + .add_element(Kind::Actor, Layer::Oa, "Control", None) + .unwrap(); + let sa = p + .add_element(Kind::System, Layer::Sa, "Radio", None) + .unwrap(); + let sb = p + .add_element(Kind::SystemActor, Layer::Sa, "Radio control", None) + .unwrap(); + let mean = p.relate(RelationKind::CommunicationMean, "Channel", oa, ob); + let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); + request.mappings = vec![ + TransitionMapping { + source: oa, + target: Some(sa), + target_kind: None, + }, + TransitionMapping { + source: ob, + target: Some(sb), + target_kind: None, + }, + ]; + let port = |alias: &str| CommunicationPortChoice::Create { + id: Id::new_v4(), + alias: alias.into(), + name: alias.into(), + description: String::new(), + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }; + request + .communication_endpoints + .push(CommunicationEndpointDecision { + source: mean, + source_port: port("radio_a"), + target_port: port("radio_b"), + }); + let plan = plan_transition(&p, &request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + p = apply_transition(&p, &plan).unwrap(); + let boundary = p.traces.values().find(|t| t.oa == mean).unwrap().sa; + let scenario = p + .add_element(Kind::Scenario, Layer::Sa, "Reverse request", None) + .unwrap(); + if let Some(SharedBody::Scenario { viewpoint, .. }) = + p.elements.get_mut(&scenario).unwrap().shared_mut() + { + *viewpoint = ScenarioViewpoint::BoundaryInterface; + } + let a = line(&mut p, scenario, sa); + let b = line(&mut p, scenario, sb); + (p, boundary, scenario, a, b) +} + +#[test] +fn mapped_unoriented_boundary_resolves_reverse_call_and_reply_from_actual_binding() { + let (mut p, boundary, scenario, a, b) = boundary_fixture(); + let (source, target) = p.relations[&boundary].body.endpoints(); + let call = message(boundary, b, a, MessageKind::Call, None); + assert_eq!( + validation::resolve_message_endpoints(&p, &call), + Resolution::Resolved { + source: target, + target: source + } + ); + let call = occurrence(&mut p, scenario, call); + let reply = message(boundary, a, b, MessageKind::Reply, Some(call)); + assert_eq!( + validation::resolve_message_endpoints(&p, &reply), + Resolution::Resolved { source, target } + ); + occurrence(&mut p, scenario, reply); + let errors: Vec<_> = validation::validate(&p) + .into_iter() + .filter(|i| i.severity == validation::Severity::Error) + .collect(); + assert!(errors.is_empty(), "{errors:?}"); +} + +#[test] +fn unoriented_flow_boundary_honors_ordinary_direction_but_preserves_correlated_reply_exception() { + let (mut p, boundary, scenario, a, b) = boundary_fixture(); + let (source, target) = p.relations[&boundary].body.endpoints(); + for (port, orientation) in [ + (source, ComponentPortOrientation::Out), + (target, ComponentPortOrientation::In), + ] { + if let Some(SharedBody::Port { contract, .. }) = + p.elements.get_mut(&port).unwrap().shared_mut() + { + *contract = PortContract::Component { + kind: ComponentPortKind::Flow, + orientation, + }; + } + } + let reverse = message(boundary, b, a, MessageKind::Call, None); + assert_eq!( + validation::resolve_message_endpoints(&p, &reverse), + Resolution::Conflict + ); + let forward = message(boundary, a, b, MessageKind::Call, None); + assert_eq!( + validation::resolve_message_endpoints(&p, &forward), + Resolution::Resolved { source, target } + ); + let call = occurrence(&mut p, scenario, forward); + let reply = message(boundary, b, a, MessageKind::Reply, Some(call)); + assert_eq!( + validation::resolve_message_endpoints(&p, &reply), + Resolution::Resolved { + source: target, + target: source + } + ); + occurrence(&mut p, scenario, reply); + let errors: Vec<_> = validation::validate(&p) + .into_iter() + .filter(|i| i.severity == validation::Severity::Error) + .collect(); + assert!(errors.is_empty(), "{errors:?}"); +} + +#[test] +fn reverse_boundary_scenario_transition_keeps_call_orientation_and_reply_identity() { + let (mut p, boundary, scenario, a, b) = boundary_fixture(); + let mean = p.traces.values().find(|t| t.sa == boundary).unwrap().oa; + let call = message(boundary, b, a, MessageKind::Call, None); + let call = occurrence(&mut p, scenario, call); + let reply = message(boundary, a, b, MessageKind::Reply, Some(call)); + let reply = occurrence(&mut p, scenario, reply); + let owner = |port| p.elements[&port].parent.unwrap(); + let mut request = TransitionRequest::new(TransitionDirection::SaToOa, vec![scenario]); + request.scope = DependencyScope::Subtree; + request.mappings = [ + (boundary, mean), + ( + owner(p.relations[&boundary].source()), + p.relations[&mean].source(), + ), + ( + owner(p.relations[&boundary].target()), + p.relations[&mean].target(), + ), + ] + .into_iter() + .map(|(source, target)| TransitionMapping { + source, + target: Some(target), + target_kind: None, + }) + .collect(); + // The explicit participant counterparts intentionally use different names. + // Review those prose differences without changing the endpoint decision. + request.resolutions = request + .mappings + .iter() + .map(|mapping| FieldResolution { + source: mapping.source, + field: TransitionField::Name, + decision: FieldDecision::KeepTarget, + }) + .collect(); + let plan = plan_transition(&p, &request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let q = apply_transition(&p, &plan).unwrap(); + let mapped = |source| q.traces.values().find(|t| t.sa == source).unwrap().oa; + let body = |id| match q.elements[&id].shared().unwrap() { + SharedBody::Occurrence { body, .. } => body, + _ => panic!("expected occurrence"), + }; + let mapped_call = mapped(call); + let mapped_reply = mapped(reply); + assert_eq!( + validation::resolve_message_endpoints(&q, body(mapped_call)), + Resolution::Resolved { + source: q.relations[&mean].target(), + target: q.relations[&mean].source(), + } + ); + assert_eq!( + validation::resolve_message_endpoints(&q, body(mapped_reply)), + Resolution::Resolved { + source: q.relations[&mean].source(), + target: q.relations[&mean].target(), + } + ); + assert!( + matches!(body(mapped_reply), OccurrenceBody::Exchange { reply_to: Some(id), .. } if *id == mapped_call) + ); + assert_eq!(q.elements[&scenario], p.elements[&scenario]); + assert_eq!(q.elements[&call], p.elements[&call]); + assert_eq!(q.elements[&reply], p.elements[&reply]); +} diff --git a/crates/oa-core/tests/communication_means.rs b/crates/oa-core/tests/communication_means.rs new file mode 100644 index 00000000..3ac80133 --- /dev/null +++ b/crates/oa-core/tests/communication_means.rs @@ -0,0 +1,515 @@ +use oa_core::{ + transitions::*, + validation::{self, MessageEndpointResolution as Resolution, Severity}, + *, +}; +use std::collections::BTreeMap; + +fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id { + p.add_element(kind, layer, name, None).unwrap() +} +fn fixture() -> (Project, Id, Id, Id) { + let mut p = Project::empty(); + let a = add(&mut p, Kind::Entity, Layer::Oa, "Rescue team"); + let b = add(&mut p, Kind::Actor, Layer::Oa, "Control centre"); + let mean = p.relate(RelationKind::CommunicationMean, "Radio", a, b); + (p, a, b, mean) +} +fn errors(p: &Project) -> Vec { + validation::validate(p) + .into_iter() + .filter(|i| i.severity == Severity::Error) + .collect() +} +fn valid(p: &Project) { + assert!(errors(p).is_empty(), "{:?}", errors(p)); +} +fn oriented(p: &mut Project, id: Id, value: bool) { + if let RelationBody::CommunicationMean { oriented, .. } = + &mut p.relations.get_mut(&id).unwrap().body + { + *oriented = value; + } +} +fn items(p: &mut Project, id: Id, values: Vec) { + match &mut p.relations.get_mut(&id).unwrap().body { + RelationBody::CommunicationMean { items, .. } | RelationBody::Interaction { items, .. } => { + *items = values + } + _ => panic!(), + } +} +fn has(p: &Project, id: Id, code: &str, severity: Severity) -> bool { + validation::validate(p) + .iter() + .any(|i| i.element == Some(id) && i.code == code && i.severity == severity) +} +fn line(p: &mut Project, scenario: Id, represented: Id) -> Id { + if let Some(SharedBody::Scenario { viewpoint, .. }) = + p.elements.get_mut(&scenario).unwrap().shared_mut() + { + *viewpoint = ScenarioViewpoint::ParticipantInteraction; + } + let id = p + .add_element( + Kind::Lifeline, + Layer::Oa, + &format!("line_{represented}"), + Some(scenario), + ) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(represented), + }; + id +} +fn message( + mean: Id, + sender: Id, + receiver: Id, + kind: MessageKind, + reply_to: Option, +) -> OccurrenceBody { + OccurrenceBody::Exchange { + exchange: Some(mean), + sender: Some(sender), + receiver: Some(receiver), + message_kind: kind, + reply_to, + values: vec![], + } +} +fn occurrence(p: &mut Project, scenario: Id, body: OccurrenceBody) -> Id { + let id = p + .add_element(Kind::Occurrence, Layer::Oa, "Message", Some(scenario)) + .unwrap(); + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { + body, + precondition: String::new(), + postcondition: String::new(), + }; + let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap(); + if !steps.contains(&id) { + steps.push(id); + } + id +} + +#[test] +fn communication_means_are_distinct_oa_participant_relations_and_strict_source6_records() { + let (mut p, a, b, mean) = fixture(); + valid(&p); + assert!(matches!( + p.relations[&mean].body, + RelationBody::CommunicationMean { + oriented: false, + .. + } + )); + p.relate(RelationKind::CommunicationMean, "Telephone", a, b); + valid(&p); + let text = language::format(&p); + assert!(text.contains("communication_mean")); + assert!(text.starts_with("arcadia 6;")); + assert_eq!(language::parse(&text).unwrap(), p); + let bytes = serde_json::to_vec(&p).unwrap(); + assert_eq!(persistence::decode(&bytes).unwrap(), p); + for version in 1..=5 { + let mut old = serde_json::to_value(&p).unwrap(); + old["schema_version"] = version.into(); + assert!( + persistence::decode(&serde_json::to_vec(&old).unwrap()).is_err(), + "schema {version}" + ); + assert!( + language::parse(&text.replacen("arcadia 6;", &format!("arcadia {version};"), 1)) + .is_err(), + "source {version}" + ); + } + let mut bad = serde_json::to_value(&p).unwrap(); + bad["relations"][mean.to_string()]["body"]["CommunicationMean"]["typo"] = true.into(); + assert!(persistence::decode(&serde_json::to_vec(&bad).unwrap()).is_err()); + for endpoint in [ + a, + add(&mut p, Kind::Role, Layer::Oa, "Role"), + add(&mut p, Kind::System, Layer::Sa, "System"), + ] { + let mut q = p.clone(); + q.relations + .get_mut(&mean) + .unwrap() + .body + .set_endpoints(a, endpoint); + assert!(!errors(&q).is_empty()); + } +} + +#[test] +fn communication_schema5_upgrade_preserves_legacy_data_and_exact_source_trivia() { + let p = Project::demo(); + let mut json = serde_json::to_value(&p).unwrap(); + json["schema_version"] = 5.into(); + for d in json["diagrams"].as_object_mut().unwrap().values_mut() { + d.as_object_mut().unwrap().remove("view_options"); + } + let result = persistence::decode_with_report(&serde_json::to_vec(&json).unwrap()).unwrap(); + assert_eq!(result.project, p); + assert_eq!(result.migration.unwrap().from_schema, 5); + let mut without_views = p.clone(); + without_views.diagrams.clear(); + let old = + language::format(&without_views).replacen("arcadia 6;", "arcadia // old contract\n5;", 1); + let bundle = source_project::SourceBundle { + entry: "entry.arcadia".into(), + files: BTreeMap::from([("entry.arcadia".into(), old.clone())]), + }; + let compiled = + source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap(); + assert_eq!( + compiled.bundle.files["entry.arcadia"], + old.replacen("\n5;", "\n6;", 1) + ); + assert_eq!(compiled.decoded.project, without_views); +} + +#[test] +fn communication_allocation_checks_whole_partial_pair_and_does_not_guess_roles() { + let (mut p, a, b, mean) = fixture(); + let x = add(&mut p, Kind::Activity, Layer::Oa, "Send"); + let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive"); + let work = p.relate(RelationKind::Interaction, "Report", x, y); + let allocation = p.relate( + RelationKind::ExchangeAllocation, + "Radio carries report", + mean, + work, + ); + let role = add(&mut p, Kind::Role, Layer::Oa, "Communicator"); + p.relate(RelationKind::RoleRealization, "Role", a, role); + p.relate(RelationKind::RoleAssignment, "Responsibility", role, x); + valid(&p); + assert!(has( + &p, + allocation, + "incomplete_exchange_allocation", + Severity::Warning + )); + p.relate(RelationKind::Allocation, "Actual receiver", a, y); + valid(&p); // reverse candidate survives + oriented(&mut p, mean, true); + assert!(has( + &p, + allocation, + "exchange_allocation_performer", + Severity::Error + )); + oriented(&mut p, mean, false); + p.relate(RelationKind::Allocation, "Actual sender", b, x); + valid(&p); + let mut q = p.clone(); + q.relations.get_mut(&mean).unwrap().body.set_endpoints(b, a); + valid(&q); + let third = add(&mut q, Kind::Entity, Layer::Oa, "Third"); + q.relations + .get_mut(&mean) + .unwrap() + .body + .set_endpoints(a, third); + assert!(has( + &q, + allocation, + "exchange_allocation_performer", + Severity::Error + )); +} + +#[test] +fn communication_allocation_items_use_specialization_and_empty_is_not_wildcard() { + let (mut p, a, b, mean) = fixture(); + let x = add(&mut p, Kind::Activity, Layer::Oa, "Send"); + let y = add(&mut p, Kind::Activity, Layer::Oa, "Receive"); + p.relate(RelationKind::Allocation, "Send allocation", a, x); + p.relate(RelationKind::Allocation, "Receive allocation", b, y); + let work = p.relate(RelationKind::Interaction, "Report", x, y); + let allocation = p.relate(RelationKind::ExchangeAllocation, "Carried", mean, work); + let base = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Report item"); + let specialized = add(&mut p, Kind::ExchangeItem, Layer::Oa, "Distress item"); + if let Some(SharedBody::ExchangeItem { supertype, .. }) = + p.elements.get_mut(&specialized).unwrap().shared_mut() + { + *supertype = Some(base); + } + items(&mut p, work, vec![specialized]); + assert!(has( + &p, + allocation, + "exchange_allocation_items", + Severity::Error + )); + items(&mut p, mean, vec![base]); + valid(&p); + items(&mut p, work, vec![base]); + items(&mut p, mean, vec![specialized]); + assert!(has( + &p, + allocation, + "exchange_allocation_items", + Severity::Error + )); +} + +#[test] +fn communication_scenario_reverse_call_and_reply_follow_actual_direction() { + let (mut p, a, b, mean) = fixture(); + let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Radio conversation"); + let la = line(&mut p, scenario, a); + let lb = line(&mut p, scenario, b); + let call_body = message(mean, lb, la, MessageKind::Call, None); + let call = occurrence(&mut p, scenario, call_body.clone()); + let reply_body = message(mean, la, lb, MessageKind::Reply, Some(call)); + occurrence(&mut p, scenario, reply_body.clone()); + assert_eq!( + validation::resolve_message_endpoints(&p, &call_body), + Resolution::Resolved { + source: b, + target: a + } + ); + assert_eq!( + validation::resolve_message_endpoints(&p, &reply_body), + Resolution::Resolved { + source: a, + target: b + } + ); + valid(&p); + let mut edited = p.relations[&mean].clone(); + if let RelationBody::CommunicationMean { oriented, .. } = &mut edited.body { + *oriented = true; + } + assert!(prepare_command(&p, &Command::EditRelation(mean, edited)).is_err()); + valid(&p); + // The established operation-response exception also allows same-means Reply on oriented means. + if let Some(SharedBody::Occurrence { body, .. }) = + p.elements.get_mut(&call).unwrap().shared_mut() + { + *body = message(mean, la, lb, MessageKind::Call, None); + } + oriented(&mut p, mean, true); + assert_eq!( + validation::resolve_message_endpoints( + &p, + &message(mean, lb, la, MessageKind::Reply, Some(call)) + ), + Resolution::Resolved { + source: b, + target: a + } + ); +} + +#[test] +fn communication_scenario_ambiguous_and_incomplete_bindings_never_guess() { + let (mut p, a, b, mean) = fixture(); + let scenario = add(&mut p, Kind::Scenario, Layer::Oa, "Roles"); + let role = add(&mut p, Kind::Role, Layer::Oa, "Shared role"); + p.relate(RelationKind::RoleRealization, "A role", a, role); + p.relate(RelationKind::RoleRealization, "B role", b, role); + let one = line(&mut p, scenario, role); + let two = line(&mut p, scenario, role); + let mut body = message(mean, one, two, MessageKind::Asynchronous, None); + assert_eq!( + validation::resolve_message_endpoints(&p, &body), + Resolution::Ambiguous + ); + assert_eq!(validation::message_endpoints(&p, &body), None); + if let OccurrenceBody::Exchange { receiver, .. } = &mut body { + *receiver = None; + } + assert_eq!( + validation::resolve_message_endpoints(&p, &body), + Resolution::Incomplete + ); + let third = add(&mut p, Kind::Entity, Layer::Oa, "Other"); + let other = line(&mut p, scenario, third); + if let OccurrenceBody::Exchange { sender, .. } = &mut body { + *sender = Some(other); + } + assert_eq!( + validation::resolve_message_endpoints(&p, &body), + Resolution::Conflict + ); +} + +fn transition_fixture() -> (Project, Id, Id, Id, TransitionRequest) { + let (mut p, a, b, mean) = fixture(); + let sa = add(&mut p, Kind::System, Layer::Sa, "System"); + let sb = add(&mut p, Kind::SystemActor, Layer::Sa, "External"); + let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); + r.mappings = vec![ + TransitionMapping { + source: a, + target: Some(sa), + target_kind: None, + }, + TransitionMapping { + source: b, + target: Some(sb), + target_kind: None, + }, + ]; + let port = |name: &str| CommunicationPortChoice::Create { + id: Id::new_v4(), + alias: name.into(), + name: name.into(), + description: "Explicit technical choice".into(), + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }; + r.communication_endpoints + .push(CommunicationEndpointDecision { + source: mean, + source_port: port("radio_source"), + target_port: port("radio_target"), + }); + (p, mean, sa, sb, r) +} + +#[test] +fn communication_transition_requires_explicit_ports_and_preserves_participant_mapping() { + let (p, mean, sa, sb, r) = transition_fixture(); + let mut incomplete = r.clone(); + incomplete.communication_endpoints.clear(); + assert!( + !plan_transition(&p, &incomplete) + .unwrap() + .conflicts + .is_empty() + ); + let plan = plan_transition(&p, &r).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let q = apply_transition(&p, &plan).unwrap(); + valid(&q); + let trace = q.traces.values().find(|t| t.oa == mean).unwrap(); + assert_eq!(trace.kind, TraceKind::Exchange); + let boundary = &q.relations[&trace.sa]; + assert_eq!(boundary.kind(), RelationKind::BoundaryExchange); + assert_eq!(q.elements[&boundary.source()].parent, Some(sa)); + assert_eq!(q.elements[&boundary.target()].parent, Some(sb)); + assert_eq!(q.relations[&mean], p.relations[&mean]); + assert_eq!(q.elements.len(), p.elements.len() + 2); + let mut collision = r.clone(); + collision.mappings[1].target = Some(sa); + assert!( + !plan_transition(&p, &collision) + .unwrap() + .conflicts + .is_empty() + ); + let mut duplicate = r.clone(); + duplicate + .communication_endpoints + .push(r.communication_endpoints[0].clone()); + assert!( + !plan_transition(&p, &duplicate) + .unwrap() + .conflicts + .is_empty() + ); +} + +#[test] +fn communication_reverse_transition_projects_owners_without_oa_ports() { + let (p, mean, sa, sb, r) = transition_fixture(); + let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap(); + let boundary = q.traces.values().find(|t| t.oa == mean).unwrap().sa; + let mut standalone = q.clone(); + standalone.traces.clear(); + let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![boundary]); + reverse.mappings = vec![ + TransitionMapping { + source: sa, + target: Some(p.relations[&mean].source()), + target_kind: None, + }, + TransitionMapping { + source: sb, + target: Some(p.relations[&mean].target()), + target_kind: None, + }, + ]; + let plan = plan_transition(&standalone, &reverse).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let result = apply_transition(&standalone, &plan).unwrap(); + assert_eq!(result.elements, standalone.elements); + let counterpart = &result.relations[&result + .traces + .values() + .find(|t| t.sa == boundary) + .unwrap() + .oa]; + assert_eq!(counterpart.kind(), RelationKind::CommunicationMean); + assert_eq!( + counterpart.body.endpoints(), + p.relations[&mean].body.endpoints() + ); +} + +#[test] +fn communication_semantic_endpoint_take_needs_scoped_decision_and_exact_history() { + let (p, mean, _, _, r) = transition_fixture(); + let q = apply_transition(&p, &plan_transition(&p, &r).unwrap()).unwrap(); + let trace = q.traces.values().find(|t| t.oa == mean).unwrap(); + let boundary = trace.sa; + let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); + transition.mappings = r.mappings.clone(); + transition.mappings.push(TransitionMapping { + source: mean, + target: Some(boundary), + target_kind: None, + }); + let mut request = SemanticTransitionRequest::new(transition); + request.references = r + .mappings + .iter() + .map(|m| (m.source, m.target.unwrap())) + .collect(); + request.reviews.push(SemanticReviewRequest { + source: mean, + resolutions: vec![FacetResolution { + facet: FacetSelector::Relation(RelationFacet::Source), + decision: SemanticDecision::TakeSource, + }], + reset: vec![], + }); + assert!( + !plan_semantic_transition(&q, request.clone()) + .unwrap() + .conflicts + .is_empty() + ); + request + .transition + .communication_endpoints + .push(CommunicationEndpointDecision { + source: mean, + source_port: CommunicationPortChoice::Reuse { + port: q.relations[&boundary].source(), + }, + target_port: CommunicationPortChoice::Reuse { + port: q.relations[&boundary].target(), + }, + }); + let plan = plan_semantic_transition(&q, request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let result = apply_semantic_transition(&q, &plan).unwrap(); + let baseline = result.traces[&trace.id] + .baseline + .as_ref() + .unwrap() + .semantic + .as_ref() + .unwrap(); + assert!(baseline.facets.iter().any(|f|matches!(f,FacetReview::Relation(RelationReview::Source(Reviewed{oa:Observed::Value(a),sa:Observed::Value(b)})) if *a==p.relations[&mean].source()&&*b==q.relations[&boundary].source()))); +} diff --git a/crates/oa-core/tests/language.rs b/crates/oa-core/tests/language.rs index 389cafe9..c24e52b3 100644 --- a/crates/oa-core/tests/language.rs +++ b/crates/oa-core/tests/language.rs @@ -339,6 +339,30 @@ project mission "Harbor safety — 港 🌊" { component_port, function_port, ); + let participants: Vec<_> = p + .elements + .values() + .filter(|e| matches!(e.kind(), Kind::Entity | Kind::Actor)) + .map(|e| e.id) + .collect(); + let means = p.relate( + RelationKind::CommunicationMean, + "Operational communication", + participants[0], + participants[1], + ); + let category = p + .add_element( + Kind::ExchangeCategory, + Layer::Oa, + "Operational exchange category", + None, + ) + .unwrap(); + *p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory { + family: oa_core::view_options::ExchangeFamily::Participant, + exchanges: vec![means], + }; p } @@ -491,7 +515,7 @@ fn authoring_defaults_and_forward_references_remain_strict() { #[test] fn typed_unknown_fields_enums_duplicates_and_reference_spelling_are_rejected() { for (source, expected) in [ - ("arcadia 6; project mission \"x\" {}", "Unsupported schema"), + ("arcadia 7; project mission \"x\" {}", "Unsupported schema"), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }", "Unknown field", @@ -795,7 +819,7 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi assert_eq!(from_source, from_json); let original: serde_json::Value = serde_json::from_slice(old_json).unwrap(); let mut retained = serde_json::to_value(&from_json).unwrap(); - assert_eq!(retained["schema_version"], 5); + assert_eq!(retained["schema_version"], oa_core::SCHEMA_VERSION); retained["schema_version"] = 2.into(); let allocations: Vec<_> = from_json .relations @@ -904,6 +928,11 @@ fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensi } } for diagram in retained["diagrams"].as_object_mut().unwrap().values_mut() { + assert_eq!( + diagram["view_options"], + serde_json::to_value(oa_core::view_options::ViewOptions::default()).unwrap() + ); + diagram.as_object_mut().unwrap().remove("view_options"); assert_eq!(diagram["routes"], serde_json::json!([])); diagram.as_object_mut().unwrap().remove("routes"); } @@ -991,7 +1020,7 @@ fn v1_text_and_json_migrate_deterministically_without_losing_legacy_semantics() assert_eq!(from_text, from_json); assert_eq!(read(text), from_text); assert_eq!(persistence::decode(json).unwrap(), from_json); - assert_eq!(from_text.schema_version, 5); + assert_eq!(from_text.schema_version, oa_core::SCHEMA_VERSION); assert!(!from_text.elements.values().any(|e| e.layer() == Layer::Sa)); let raw: serde_json::Value = serde_json::from_slice(json).unwrap(); for (id, record) in raw["elements"].as_object().unwrap() { @@ -1193,7 +1222,7 @@ fn complete_normative_examples_are_accepted_by_the_real_parser() { if code.trim_start().starts_with("arcadia 2;") || code.trim_start().starts_with("arcadia 3;") || code.trim_start().starts_with("arcadia 4;") - || code.trim_start().starts_with("arcadia 5;") + || code.trim_start().starts_with("arcadia 6;") { read(&code); examples += 1; diff --git a/crates/oa-core/tests/port_migration.rs b/crates/oa-core/tests/port_migration.rs index 9146fdd4..e6ef29c3 100644 --- a/crates/oa-core/tests/port_migration.rs +++ b/crates/oa-core/tests/port_migration.rs @@ -39,7 +39,10 @@ fn frozen_original_v2_reader_preserves_every_original_record() { assert_eq!(decoded.project, from_source.project); assert_eq!(decoded.migration, from_source.migration); let report = decoded.migration.unwrap(); - assert_eq!((report.from_schema, report.to_schema), (2, 5)); + assert_eq!( + (report.from_schema, report.to_schema), + (2, oa_core::SCHEMA_VERSION) + ); assert_eq!(report.preserved_ports.len(), 4); assert!(report.generated_endpoints.is_empty()); assert_eq!(report.orientation_defaults.len(), 1); @@ -60,6 +63,11 @@ fn frozen_original_v2_reader_preserves_every_original_record() { ); let mut current = serde_json::to_value(&decoded.project).unwrap(); for diagram in current["diagrams"].as_object_mut().unwrap().values_mut() { + assert_eq!( + diagram["view_options"], + serde_json::to_value(oa_core::view_options::ViewOptions::default()).unwrap() + ); + diagram.as_object_mut().unwrap().remove("view_options"); assert_eq!(diagram["routes"], json!([]), "V2 adds no authored routing"); diagram.as_object_mut().unwrap().remove("routes"); } @@ -144,7 +152,7 @@ fn missing_v2_endpoints_are_deterministic_reported_and_independently_typed() { } } let canonical = language::format(&first.project); - assert!(canonical.starts_with("arcadia 5;")); + assert!(canonical.starts_with("arcadia 6;")); let reopened = persistence::decode_with_report(canonical.as_bytes()).unwrap(); assert!(reopened.migration.is_none()); assert_eq!(reopened.project, first.project); @@ -274,7 +282,7 @@ fn old_contract_is_validated_before_new_shapes_and_v1_flows_through_v2() { assert!(error.contains("V2") && error.contains(&fe), "{error}"); let v1 = include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json"); let result = persistence::decode_with_report(v1).unwrap(); - assert_eq!(result.project.schema_version, 5); + assert_eq!(result.project.schema_version, oa_core::SCHEMA_VERSION); assert_eq!(result.migration.unwrap().from_schema, 1); assert_eq!(persistence::decode(v1).unwrap(), result.project); } diff --git a/crates/oa-core/tests/port_source.rs b/crates/oa-core/tests/port_source.rs index cc064586..b98aba04 100644 --- a/crates/oa-core/tests/port_source.rs +++ b/crates/oa-core/tests/port_source.rs @@ -30,7 +30,10 @@ project typed_ports "Typed ports" { fn source4_covers_typed_contracts_roles_allocations_and_nested_legacy_references() { let decoded = language::parse_with_report(SOURCE).unwrap(); let migration = decoded.migration.as_ref().unwrap(); - assert_eq!((migration.from_schema, migration.to_schema), (4, 5)); + assert_eq!( + (migration.from_schema, migration.to_schema), + (4, oa_core::SCHEMA_VERSION) + ); let canonical = language::format(&decoded.project); for field in [ "incoming_items [@data]", diff --git a/crates/oa-core/tests/r3_constructor_ack.rs b/crates/oa-core/tests/r3_constructor_ack.rs index b697b68a..1bd7c4c2 100644 --- a/crates/oa-core/tests/r3_constructor_ack.rs +++ b/crates/oa-core/tests/r3_constructor_ack.rs @@ -351,7 +351,10 @@ fn migrated_equal_constructor_can_complete_semantic_review_without_model_changes for reverse in [false, true] { let decoded = persistence::decode_with_report(json.as_bytes()).unwrap(); let report = decoded.migration.unwrap(); - assert_eq!((report.from_schema, report.to_schema), (4, 5)); + assert_eq!( + (report.from_schema, report.to_schema), + (4, oa_core::SCHEMA_VERSION) + ); let p = decoded.project; let t = p.traces.values().next().unwrap(); assert!(t.baseline.as_ref().unwrap().semantic.is_none()); diff --git a/crates/oa-core/tests/r3_migration.rs b/crates/oa-core/tests/r3_migration.rs index df6efabf..f7a9e4b6 100644 --- a/crates/oa-core/tests/r3_migration.rs +++ b/crates/oa-core/tests/r3_migration.rs @@ -11,9 +11,12 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti let source = language::parse_with_report(SOURCE4).unwrap(); assert_eq!(json.project, source.project); for decoded in [json, source] { - assert_eq!(decoded.project.schema_version, 5); + assert_eq!(decoded.project.schema_version, oa_core::SCHEMA_VERSION); let report = decoded.migration.unwrap(); - assert_eq!((report.from_schema, report.to_schema), (4, 5)); + assert_eq!( + (report.from_schema, report.to_schema), + (4, oa_core::SCHEMA_VERSION) + ); let trace = decoded.project.traces.values().next().unwrap(); let baseline = trace.baseline.as_ref().unwrap(); assert!(baseline.semantic.is_none()); @@ -22,7 +25,7 @@ fn frozen_v4_json_and_source_preserve_the_old_review_without_fabricating_semanti assert_eq!(baseline.sa.name, "Approved SA name"); assert_ne!(baseline.oa.name, decoded.project.named(trace.oa)); let mut expected: Value = serde_json::from_slice(JSON4).unwrap(); - expected["schema_version"] = 5.into(); + expected["schema_version"] = oa_core::SCHEMA_VERSION.into(); assert_eq!(serde_json::to_value(&decoded.project).unwrap(), expected); let reopened = persistence::decode_with_report(&serde_json::to_vec(&decoded.project).unwrap()) @@ -117,7 +120,7 @@ fn authored_v4_modules_upgrade_only_the_version_and_preserve_nested_comments() { assert_eq!(compiled.bundle.files["contracts.arcadia"], module); assert_eq!( compiled.bundle.files["project.arcadia"], - entry.replacen("arcadia 4;", "arcadia 5;", 1) + entry.replacen("arcadia 4;", "arcadia 6;", 1) ); assert_eq!( compiled.index.lexical_owners @@ -152,8 +155,11 @@ fn original_v1_v2_v3_inputs_complete_the_explicit_upgrade_to_five() { ] { let decoded = persistence::decode_with_report(bytes).unwrap(); let report = decoded.migration.unwrap(); - assert_eq!((report.from_schema, report.to_schema), (version, 5)); - assert_eq!(decoded.project.schema_version, 5); + assert_eq!( + (report.from_schema, report.to_schema), + (version, oa_core::SCHEMA_VERSION) + ); + assert_eq!(decoded.project.schema_version, oa_core::SCHEMA_VERSION); assert!( decoded .project diff --git a/crates/oa-core/tests/r3_source.rs b/crates/oa-core/tests/r3_source.rs index 21cde40c..3f3a015a 100644 --- a/crates/oa-core/tests/r3_source.rs +++ b/crates/oa-core/tests/r3_source.rs @@ -131,7 +131,7 @@ fn typed_historical_facets_roundtrip_deleted_references_and_all_leaf_categories( ]); assert!(!model.contains(id(90))); let source = language::format(&model); - assert!(source.starts_with("arcadia 5;")); + assert!(source.starts_with("arcadia 6;")); assert!( source.contains(&format!("Named uuid \"{}\"", id(90))), "{source}" diff --git a/crates/oa-core/tests/r6_migration.rs b/crates/oa-core/tests/r6_migration.rs index 886cf173..8e6ce953 100644 --- a/crates/oa-core/tests/r6_migration.rs +++ b/crates/oa-core/tests/r6_migration.rs @@ -21,7 +21,10 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships() assert_eq!(decoded.migration, source.migration); let project = &decoded.project; let report = decoded.migration.as_ref().unwrap(); - assert_eq!((report.from_schema, report.to_schema), (3, 5)); + assert_eq!( + (report.from_schema, report.to_schema), + (3, oa_core::SCHEMA_VERSION) + ); assert_eq!(project.identities().len(), 19); assert!(report.preserved_ports.is_empty()); assert!(report.generated_endpoints.is_empty()); @@ -71,7 +74,7 @@ fn genuine_v3_preserves_typed_ports_and_allocates_deterministic_relationships() .any(|issue| issue.code == "message_value_ambiguous_path") ); let canonical = language::format(project); - assert!(canonical.starts_with("arcadia 5;")); + assert!(canonical.starts_with("arcadia 6;")); let reopened = language::parse_with_report(&canonical).unwrap(); assert_eq!(&reopened.project, project); assert!(reopened.migration.is_none()); @@ -154,12 +157,12 @@ fn v1_and_frozen_v2_inputs_reach_four_through_the_shared_data_upgrade() { language::parse_with_report(include_str!("fixtures/v2-coastal-rescue.arcadia")).unwrap(); assert_eq!(v2.project, text.project); assert_eq!(v2.migration, text.migration); - assert_eq!(v2.migration.unwrap().to_schema, 5); + assert_eq!(v2.migration.unwrap().to_schema, oa_core::SCHEMA_VERSION); let v1 = language::parse_with_report( "arcadia 1; project \"Old\" id \"00000000-0000-0000-0000-000000000001\" {}", ) .unwrap(); - assert_eq!(v1.project.schema_version, 5); + assert_eq!(v1.project.schema_version, oa_core::SCHEMA_VERSION); assert_eq!(v1.migration.unwrap().from_schema, 1); } diff --git a/crates/oa-core/tests/r6_source.rs b/crates/oa-core/tests/r6_source.rs index 0335b26a..461f86c3 100644 --- a/crates/oa-core/tests/r6_source.rs +++ b/crates/oa-core/tests/r6_source.rs @@ -26,7 +26,7 @@ fn authored_v3_bundle_upgrade_preserves_includes_nested_ownership_comments_and_i }; let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); assert_eq!(compiled.bundle.files.len(), 2); - assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 5;")); + assert!(compiled.bundle.files["project.arcadia"].contains("arcadia 6;")); assert!(compiled.bundle.files["project.arcadia"].contains("include \"contracts.arcadia\";")); for text in [ "// Contracts stay in their original module.", diff --git a/crates/oa-core/tests/release1_teaching.rs b/crates/oa-core/tests/release1_teaching.rs new file mode 100644 index 00000000..3b75d61e --- /dev/null +++ b/crates/oa-core/tests/release1_teaching.rs @@ -0,0 +1,256 @@ +//! Engineering oracles for the distributable current-profile lesson. +#[path = "../../../examples/release1/model.rs"] +mod teaching; +use oa_core::{projection, source_project, transitions::*, validation, *}; +use teaching::id; + +#[test] +fn release1_teaching_regeneration_and_shipped_source_are_exact_with_one_intentional_warning() { + let first = teaching::build(); + let second = teaching::build(); + assert_eq!( + first.project, second.project, + "fresh planner UUIDs must not churn the teaching source" + ); + assert_eq!(first.before_transition, second.before_transition); + assert_eq!(first.request, second.request); + let bundle = teaching::checked_bundle(&first.project); + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../examples/release1"); + for (name, bytes) in bundle.files { + assert_eq!( + std::fs::read_to_string(root.join(&name)).unwrap(), + bytes, + "regenerate {name}" + ); + } + assert_eq!( + persistence::decode(&std::fs::read(root.join("model.json")).unwrap()).unwrap(), + first.project + ); + for (path, expected) in [ + ( + "transition-before.json", + serde_json::to_value(&first.before_transition).unwrap(), + ), + ( + "transition-request.json", + serde_json::to_value(&first.request).unwrap(), + ), + ( + "expected-diagnostics.json", + serde_json::to_value(validation::validate(&first.project)).unwrap(), + ), + ] { + let shipped: serde_json::Value = + serde_json::from_slice(&std::fs::read(root.join(path)).unwrap()).unwrap(); + assert_eq!(shipped, expected, "regenerate {path}"); + } + let issues = validation::validate(&first.project); + assert_eq!(issues.len(), 1, "{issues:?}"); + assert_eq!(issues[0].severity, validation::Severity::Warning); + assert_eq!(issues[0].code, "work_without_behavior_context"); + assert_eq!(issues[0].element, Some(id("sa_audit_thresholds"))); + assert!( + first + .project + .elements + .values() + .filter(|e| e.layer() == Layer::Oa) + .all(|e| e.kind() != Kind::Port) + ); +} + +#[test] +fn release1_teaching_preserves_explicit_transport_direction_reuse_and_view_identity() { + let lesson = teaching::build(); + let p = &lesson.project; + for prefix in ["oa", "sa"] { + assert!( + matches!(p.elements[&id(&format!("{prefix}_assess_step"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(target) }, .. }) if *target == id(&format!("{prefix}_assessment_procedure"))) + ); + assert!( + matches!(p.elements[&id(&format!("{prefix}_assessment_action"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Action { action: Some(target), .. }, .. }) if *target == id(&format!("{prefix}_assess"))) + ); + assert!( + p.relations + .values() + .any(|relation| relation.kind() == RelationKind::ChainInvolvement + && relation.source() == id(&format!("{prefix}_assessment_procedure")) + && relation.target() == id(&format!("{prefix}_triage"))) + ); + assert!( + matches!(p.elements[&id(&format!("{prefix}_conversation"))].shared(), Some(SharedBody::Scenario { context: Some(context), .. }) if *context == id(&format!("{prefix}_restore"))) + ); + assert!(p.relations.values().any(|relation| relation.kind() + == RelationKind::CapabilityInvolvement + && relation.source() == id(&format!("{prefix}_assess")) + && relation.target() == id(&format!("{prefix}_triage")))); + let call = &p.elements[&id(&format!("{prefix}_call"))]; + let reply = &p.elements[&id(&format!("{prefix}_reply"))]; + let body = |e: &Element| { + if let Some(SharedBody::Occurrence { body, .. }) = e.shared() { + body.clone() + } else { + panic!() + } + }; + let validation::MessageEndpointResolution::Resolved { source, target } = + validation::resolve_message_endpoints(p, &body(call)) + else { + panic!("call not resolved") + }; + assert_eq!( + validation::resolve_message_endpoints(p, &body(reply)), + validation::MessageEndpointResolution::Resolved { + source: target, + target: source + } + ); + assert!( + matches!(p.elements[&id(&format!("{prefix}_use_field"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(target) }, .. }) if *target == id(&format!("{prefix}_field_procedure"))) + ); + assert!( + matches!(p.elements[&id(&format!("{prefix}_use_conversation"))].shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::ScenarioUse { scenario: Some(target), bindings }, .. }) if *target == id(&format!("{prefix}_conversation")) && bindings.len() == 2) + ); + } + let full = projection::project_view(p, id("sa_allocation_view")); + let focused = projection::project_view(p, id("sa_service_view")); + let edges: std::collections::BTreeSet<_> = full.edges.iter().map(|e| e.id).collect(); + assert_eq!( + edges, + [ + id("sa_contact"), + id("sa_repair_request"), + id("sa_repair_report") + ] + .into() + ); + assert_eq!(focused.edges.len(), 1); + assert_eq!(focused.edges[0].id, id("sa_contact")); + assert!( + p.diagrams[&id("sa_service_view")] + .view_options + .exchange_labels + .label(p, focused.edges[0].id) + .contains("Valve repair") + ); + let ports = |view: &projection::ProjectedView| { + view.ports + .iter() + .map(|v| (v.element, v.position)) + .collect::>() + }; + assert_eq!( + ports(&full), + ports(&focused), + "filtering must not move the shared technical port geometry" + ); + assert!( + p.traces + .values() + .any(|t| t.oa == id("oa_contact") && t.sa == id("sa_contact")) + ); +} + +#[test] +fn release1_teaching_rejects_guessed_ports_and_wrong_performer_without_changing_source() { + let lesson = teaching::build(); + let mut missing_decisions = lesson.request.clone(); + missing_decisions.communication_endpoints.clear(); + let plan = plan_transition(&lesson.before_transition, &missing_decisions).unwrap(); + assert!( + !plan.conflicts.is_empty(), + "an operational channel does not determine technical ports" + ); + let compiled = source_project::compile_bundle( + &teaching::checked_bundle(&lesson.project), + source_project::ProjectLimits::default(), + ) + .unwrap(); + let mut wrong = lesson.project.relations[&id("oa_repair_request")].clone(); + wrong.body.set_endpoints(id("oa_assess"), id("oa_reset")); + assert!( + execute_batch( + &lesson.project, + &[Command::EditRelation(wrong.id, wrong.clone())] + ) + .is_err() + ); + let mut candidate = lesson.project.clone(); + candidate.relations.insert(wrong.id, wrong); + assert!( + validation::validate(&candidate) + .iter() + .any(|i| i.severity == validation::Severity::Error + && i.code == "exchange_allocation_performer") + ); + assert!( + source_project::update_bundle( + &compiled, + &candidate, + source_project::ProjectLimits::default() + ) + .is_err() + ); + assert_eq!(compiled.decoded.project, lesson.project); +} + +#[test] +fn release1_teaching_review_changes_only_the_selected_counterpart_and_preserves_modules() { + let lesson = teaching::build(); + let mut changed = lesson.project.clone(); + changed.elements.get_mut(&id("oa_confirm")).unwrap().name = "Confirm stable pressure".into(); + let compiled = source_project::compile_bundle( + &teaching::checked_bundle(&changed), + source_project::ProjectLimits::default(), + ) + .unwrap(); + let mut transition = + TransitionRequest::new(TransitionDirection::OaToSa, vec![id("oa_confirm")]); + transition.fields.clear(); + transition.mappings.push(TransitionMapping { + source: id("oa_confirm"), + target: Some(id("sa_confirm")), + target_kind: None, + }); + let mut request = SemanticTransitionRequest::new(transition); + request.reviews.push(SemanticReviewRequest { + source: id("oa_confirm"), + resolutions: vec![FacetResolution { + facet: FacetSelector::Header(HeaderFacet::Name), + decision: SemanticDecision::TakeSource, + }], + reset: vec![], + }); + let plan = plan_semantic_transition(&changed, request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let accepted = apply_semantic_transition(&changed, &plan).unwrap(); + assert_eq!( + accepted.elements[&id("sa_confirm")].name, + "Confirm stable pressure" + ); + assert_eq!( + accepted.elements[&id("sa_control")].name, + lesson.project.elements[&id("sa_control")].name + ); + let updated = source_project::update_bundle( + &compiled, + &accepted, + source_project::ProjectLimits::default(), + ) + .unwrap(); + assert_eq!(updated.decoded.project, accepted); + assert_eq!( + updated.bundle.files.keys().collect::>(), + compiled.bundle.files.keys().collect::>() + ); + for (path, original) in &compiled.bundle.files { + if path != "sa/functions/function_sa_restore_work.arcadia" && path != "traceability.arcadia" + { + assert_eq!( + &updated.bundle.files[path], original, + "unrelated module changed: {path}" + ); + } + } +} diff --git a/crates/oa-core/tests/release_ports.rs b/crates/oa-core/tests/release_ports.rs index c2bee3f1..9673b478 100644 --- a/crates/oa-core/tests/release_ports.rs +++ b/crates/oa-core/tests/release_ports.rs @@ -733,12 +733,36 @@ fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() { 0 ); let (mut p, a, b, _, fe) = fixture(); - allocate(&mut p, a, b); + let (system, partner) = allocate(&mut p, a, b); let component_req = component_request(&p, fe, ComponentPortKind::Flow); let boundary = boundary_id(&component_req); p = execute_batch(&p, &plan_component_exchange(&p, &component_req).unwrap()).unwrap(); let mut reverse = TransitionRequest::new(TransitionDirection::SaToOa, vec![fe, boundary]); reverse.scope = DependencyScope::Required; + // A boundary now has a portless OA CommunicationMean counterpart. Its + // participant owners require an explicit operational boundary decision. + let incomplete = plan_transition(&p, &reverse).unwrap(); + for owner in [system, partner] { + assert!( + incomplete + .conflicts + .iter() + .any(|conflict| conflict.source == Some(owner) + && conflict.message.contains("counterpart kind explicitly")) + ); + } + reverse.mappings = vec![ + TransitionMapping { + source: system, + target: None, + target_kind: Some(Kind::Entity), + }, + TransitionMapping { + source: partner, + target: None, + target_kind: Some(Kind::Actor), + }, + ]; let plan = plan_transition(&p, &reverse).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); assert!(plan.warnings.iter().any(|w| w.contains("SA-only omission"))); @@ -757,6 +781,28 @@ fn transitions_create_sa_ports_and_omit_sa_implementation_from_portless_oa() { .values() .any(|r| r.kind() == RelationKind::Interaction) ); + let mean = result + .traces + .values() + .find(|trace| trace.sa == boundary) + .unwrap() + .oa; + let relation = &result.relations[&mean]; + assert_eq!(relation.kind(), RelationKind::CommunicationMean); + assert_eq!(result.elements[&relation.source()].kind(), Kind::Entity); + assert_eq!(result.elements[&relation.target()].kind(), Kind::Actor); + assert!( + result + .traces + .values() + .any(|trace| trace.sa == system && trace.oa == relation.source()) + ); + assert!( + result + .traces + .values() + .any(|trace| trace.sa == partner && trace.oa == relation.target()) + ); for e in p.elements.values() { assert_eq!(result.elements[&e.id], *e); } diff --git a/crates/oa-core/tests/release_routing.rs b/crates/oa-core/tests/release_routing.rs index 078438d7..c7e596c1 100644 --- a/crates/oa-core/tests/release_routing.rs +++ b/crates/oa-core/tests/release_routing.rs @@ -39,6 +39,7 @@ fn fixture() -> Fixture { description: String::new(), layer: Layer::Sa, projection: ProjectionKind::Overview, + view_options: Default::default(), nodes: BTreeMap::new(), routes: vec![], }, diff --git a/crates/oa-core/tests/semantic_artifact_absence_oracle.rs b/crates/oa-core/tests/semantic_artifact_absence_oracle.rs new file mode 100644 index 00000000..e1753690 --- /dev/null +++ b/crates/oa-core/tests/semantic_artifact_absence_oracle.rs @@ -0,0 +1,140 @@ +//! Independent GUI boundary: Presence observes attachment, not record existence. +use oa_core::{transitions::*, *}; + +fn artifact(p: &mut Project, owner: Id, name: &str) -> Id { + let id = Id::new_v4(); + p.artifact_links.insert( + id, + ArtifactLink { + id, + alias: format!("artifact_{}", id.simple()), + name: name.into(), + description: String::new(), + owner, + relation: ArtifactRelation::References, + uri: "urn:engineering:requirement".into(), + revision: None, + fragment: Some(String::new()), + }, + ); + id +} + +fn fixture(reverse: bool) -> (Project, Id, Id, TransitionDirection) { + let mut p = Project::empty(); + let oa = p + .add_element(Kind::Activity, Layer::Oa, "OA owner", None) + .unwrap(); + let sa = p + .add_element(Kind::Function, Layer::Sa, "SA owner", None) + .unwrap(); + let id = Id::new_v4(); + p.traces.insert( + id, + Trace { + id, + alias: format!("trace_{}", id.simple()), + name: "Owner review".into(), + description: String::new(), + oa, + sa, + kind: TraceKind::Manual, + baseline: Some(TraceBaseline { + oa: p.properties(oa).unwrap(), + sa: p.properties(sa).unwrap(), + semantic: None, + }), + }, + ); + if reverse { + (p, sa, oa, TransitionDirection::SaToOa) + } else { + (p, oa, sa, TransitionDirection::OaToSa) + } +} + +fn removal( + source: Id, + target: Id, + direction: TransitionDirection, + source_link: Option, + target_link: Id, +) -> SemanticTransitionRequest { + let mut transition = TransitionRequest::new(direction, vec![source]); + transition.mappings.push(TransitionMapping { + source, + target: Some(target), + target_kind: None, + }); + let mut request = SemanticTransitionRequest::new(transition); + let keys = if direction == TransitionDirection::OaToSa { + KeyPair { + oa: source_link, + sa: Some(target_link), + } + } else { + KeyPair { + oa: Some(target_link), + sa: source_link, + } + }; + if let (Some(oa), Some(sa)) = (keys.oa, keys.sa) { + request.references.insert(oa, sa); + } + request.reviews.push(SemanticReviewRequest { + source, + reset: vec![], + resolutions: vec![FacetResolution { + facet: FacetSelector::Artifact { + keys, + facet: ArtifactFacet::Presence, + }, + decision: SemanticDecision::TakeSource, + }], + }); + request +} + +#[test] +fn target_only_artifact_take_preserves_without_fabricated_source_identity() { + for reverse in [false, true] { + let (mut p, source, target, direction) = fixture(reverse); + let link = artifact(&mut p, target, "Target-only requirement"); + let before = p.clone(); + let plan = + plan_semantic_transition(&p, removal(source, target, direction, None, link)).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + assert_eq!(p, before, "planning must be pure"); + let after = apply_semantic_transition(&p, &plan).unwrap(); + assert_eq!(after.artifact_links, before.artifact_links); + assert_eq!(after.elements, before.elements); + assert_eq!(after.relations, before.relations); + } +} + +#[test] +fn moved_away_source_artifact_counts_as_absent_from_reviewed_owner() { + for reverse in [false, true] { + let (mut p, source, target, direction) = fixture(reverse); + let (kind, layer) = if reverse { + (Kind::Function, Layer::Sa) + } else { + (Kind::Activity, Layer::Oa) + }; + let elsewhere = p + .add_element(kind, layer, "Moved source owner", None) + .unwrap(); + let source_link = artifact(&mut p, elsewhere, "Still exists elsewhere"); + let target_link = artifact(&mut p, target, "Old counterpart attachment"); + let original_source = p.artifact_links[&source_link].clone(); + let plan = plan_semantic_transition( + &p, + removal(source, target, direction, Some(source_link), target_link), + ) + .unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let after = apply_semantic_transition(&p, &plan).unwrap(); + assert!(!after.artifact_links.contains_key(&target_link)); + assert_eq!(after.artifact_links[&source_link], original_source); + } +} diff --git a/crates/oa-core/tests/semantic_attachments_gui_oracle.rs b/crates/oa-core/tests/semantic_attachments_gui_oracle.rs new file mode 100644 index 00000000..611cc1c6 --- /dev/null +++ b/crates/oa-core/tests/semantic_attachments_gui_oracle.rs @@ -0,0 +1,398 @@ +//! Direct core oracles for the next existing-record graphical attachment slice. +use oa_core::{transitions::*, *}; + +struct Fixture { + p: Project, + owners: [Id; 2], + attached: [Id; 2], + outside: [Id; 2], + trace: Id, + child: bool, + end: IncidentEnd, +} +fn trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id { + let id = Id::new_v4(); + p.traces.insert( + id, + Trace { + id, + alias: format!("trace_{}", id.simple()), + name: name.into(), + description: String::new(), + oa, + sa, + kind: TraceKind::Manual, + baseline: None, + }, + ); + id +} +fn fixture(child: bool, end: IncidentEnd) -> Fixture { + let mut p = Project::empty(); + let mut owners = vec![]; + let mut attached = vec![]; + let mut outside = vec![]; + for layer in [Layer::Oa, Layer::Sa] { + let kind = if child { + if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + } + } else { + Kind::DataType + }; + let owner = p.add_element(kind, layer, "Current owner", None).unwrap(); + let replacement = p + .add_element(kind, layer, "Reviewed replacement", None) + .unwrap(); + let record = if child { + p.add_element(kind, layer, "Attached work", Some(owner)) + .unwrap() + } else { + let peer = p.add_element(kind, layer, "Other endpoint", None).unwrap(); + let (source, target) = if end == IncidentEnd::Source { + (owner, peer) + } else { + (peer, owner) + }; + p.relate( + RelationKind::DataAssociation, + "Incident data association", + source, + target, + ) + }; + owners.push(owner); + attached.push(record); + outside.push(replacement); + } + for element in p.elements.values_mut() { + if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() { + *definition = DataTypeDefinition::Record { supertype: None }; + } + } + let trace = trace(&mut p, owners[0], owners[1], "Owner attachment rationale"); + validate_candidate(&p).unwrap(); + Fixture { + p, + owners: [owners[0], owners[1]], + attached: [attached[0], attached[1]], + outside: [outside[0], outside[1]], + trace, + child, + end, + } +} +fn selector(f: &Fixture) -> FacetSelector { + let keys = KeyPair { + oa: Some(f.attached[0]), + sa: Some(f.attached[1]), + }; + if f.child { + FacetSelector::Child(keys) + } else { + FacetSelector::Incident { keys, end: f.end } + } +} +fn candidate(f: &Fixture, side: usize) -> SemanticRecord { + if f.child { + let mut value = f.p.elements[&f.attached[side]].clone(); + value.parent = Some(f.outside[side]); + SemanticRecord::Element(value) + } else { + let mut value = f.p.relations[&f.attached[side]].clone(); + let (a, b) = value.body.endpoints(); + value.body.set_endpoints( + if f.end == IncidentEnd::Source { + f.outside[side] + } else { + a + }, + if f.end == IncidentEnd::Target { + f.outside[side] + } else { + b + }, + ); + SemanticRecord::Relation(value) + } +} +fn request(f: &Fixture, reverse: bool, decision: SemanticDecision) -> SemanticTransitionRequest { + let source = usize::from(reverse); + let target = 1 - source; + let mut transition = TransitionRequest::new( + if reverse { + TransitionDirection::SaToOa + } else { + TransitionDirection::OaToSa + }, + vec![f.owners[source]], + ); + transition.mappings.push(TransitionMapping { + source: f.owners[source], + target: Some(f.owners[target]), + target_kind: None, + }); + let mut result = SemanticTransitionRequest::new(transition); + result.traces.insert(f.owners[source], f.trace); + result.references.insert(f.owners[0], f.owners[1]); + result.references.insert(f.attached[0], f.attached[1]); + result.reviews.push(SemanticReviewRequest { + source: f.owners[source], + resolutions: vec![FacetResolution { + facet: selector(f), + decision, + }], + reset: vec![], + }); + result +} +fn prepared(p: &Project, request: SemanticTransitionRequest) -> (Project, SemanticTransitionPlan) { + let plan = plan_semantic_transition(p, request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let result = prepare_semantic_transition(p, &plan).unwrap().project; + (result, plan) +} +#[test] +fn attachment_candidate_changes_existing_parent_or_endpoint_without_attached_record_trace() { + for child in [true, false] { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + if child && end == IncidentEnd::Target { + continue; + } + for reverse in [false, true] { + let f = fixture(child, end); + assert_eq!(f.p.traces.len(), 1); + let side = usize::from(!reverse); + let expected = candidate(&f, side); + let mut request = request(&f, reverse, SemanticDecision::Candidate); + request.candidates.push(expected.clone()); + let original_source = semantic_record(&f.p, f.attached[1 - side]); + let (result, plan) = prepared(&f.p, request); + assert_eq!(semantic_record(&result, f.attached[side]), Some(expected)); + assert_eq!( + semantic_record(&result, f.attached[1 - side]), + original_source + ); + assert_eq!( + result.traces.len(), + 1, + "No relation/child trace is invented" + ); + assert_eq!(plan.reviews[0].acknowledged, vec![selector(&f)]); + let source = language::format(&result); + assert_eq!(language::parse(&source).unwrap(), result); + let mut document = f.p.clone(); + let receipt = apply_semantic_transition(&document, &plan).unwrap(); + document = receipt; + assert_eq!(document, result); + } + } + } +} +#[test] +fn incident_absence_take_refuses_but_explicit_candidate_endpoint_is_reviewable() { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + for reverse in [false, true] { + let mut f = fixture(false, end); + let source = usize::from(reverse); + let SemanticRecord::Relation(detached) = candidate(&f, source) else { + unreachable!() + }; + f.p.relations.insert(f.attached[source], detached); + let take = + plan_semantic_transition(&f.p, request(&f, reverse, SemanticDecision::TakeSource)) + .unwrap(); + assert!( + take.conflicts + .iter() + .any(|e| e.contains("explicit final endpoint")), + "{:?}", + take.conflicts + ); + let mut request = request(&f, reverse, SemanticDecision::Candidate); + request.candidates.push(candidate(&f, 1 - source)); + let _ = prepared(&f.p, request); + } + } +} +#[test] +fn attachment_candidate_and_direct_record_keep_conflict_by_actual_slot_in_either_order() { + for child in [true, false] { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + if child && end == IncidentEnd::Target { + continue; + } + for reverse in [false, true] { + let mut f = fixture(child, end); + let linked = trace( + &mut f.p, + f.attached[0], + f.attached[1], + "Independent record rationale", + ); + let source = usize::from(reverse); + let target = 1 - source; + let mut request = request(&f, reverse, SemanticDecision::Candidate); + request.candidates.push(candidate(&f, target)); + request.transition.sources.push(f.attached[source]); + request.transition.mappings.push(TransitionMapping { + source: f.attached[source], + target: Some(f.attached[target]), + target_kind: None, + }); + request.traces.insert(f.attached[source], linked); + let facet = if child { + FacetSelector::Header(HeaderFacet::Parent) + } else { + FacetSelector::Relation(if end == IncidentEnd::Source { + RelationFacet::Source + } else { + RelationFacet::Target + }) + }; + request.reviews.push(SemanticReviewRequest { + source: f.attached[source], + resolutions: vec![FacetResolution { + facet, + decision: SemanticDecision::KeepTarget, + }], + reset: vec![], + }); + for permute in [false, true] { + let mut request = request.clone(); + if permute { + request.reviews.reverse(); + request.transition.sources.reverse(); + request.transition.mappings.reverse(); + } + let plan = plan_semantic_transition(&f.p, request).unwrap(); + assert!( + plan.conflicts.iter().any(|e| e.contains("Contradictory")), + "{child}/{end:?}/{reverse}: {:?}", + plan.conflicts + ); + } + } + } + } +} + +#[test] +fn convergent_owner_attachment_candidates_agree_and_keep_cannot_override_them() { + for child in [true, false] { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + if child && end == IncidentEnd::Target { + continue; + } + let mut f = fixture(child, end); + let second_owner = + f.p.add_element( + f.p.elements[&f.owners[0]].kind(), + Layer::Oa, + "Second operational concern", + None, + ) + .unwrap(); + if let Some(SharedBody::DataType { definition, .. }) = + f.p.elements.get_mut(&second_owner).unwrap().shared_mut() + { + *definition = DataTypeDefinition::Record { supertype: None }; + } + let second_record = Id::new_v4(); + if child { + let mut value = f.p.elements[&f.attached[0]].clone(); + value.id = second_record; + value.alias = format!("second_{}", second_record.simple()); + value.parent = Some(second_owner); + f.p.elements.insert(second_record, value); + } else { + let mut value = f.p.relations[&f.attached[0]].clone(); + value.id = second_record; + value.alias = format!("second_{}", second_record.simple()); + let (a, b) = value.body.endpoints(); + value.body.set_endpoints( + if end == IncidentEnd::Source { + second_owner + } else { + a + }, + if end == IncidentEnd::Target { + second_owner + } else { + b + }, + ); + f.p.relations.insert(second_record, value); + } + let second_trace = trace( + &mut f.p, + second_owner, + f.owners[1], + "Second owner correspondence", + ); + validate_candidate(&f.p).unwrap(); + let mut request = request(&f, false, SemanticDecision::Candidate); + request.candidates.push(candidate(&f, 1)); + request.transition.sources.push(second_owner); + request.transition.mappings.push(TransitionMapping { + source: second_owner, + target: Some(f.owners[1]), + target_kind: None, + }); + request.references.insert(second_owner, f.owners[1]); + request.references.insert(second_record, f.attached[1]); + request.traces.insert(second_owner, second_trace); + let keys = KeyPair { + oa: Some(second_record), + sa: Some(f.attached[1]), + }; + let facet = if child { + FacetSelector::Child(keys) + } else { + FacetSelector::Incident { keys, end } + }; + request.reviews.push(SemanticReviewRequest { + source: second_owner, + resolutions: vec![FacetResolution { + facet, + decision: SemanticDecision::Candidate, + }], + reset: vec![], + }); + let (agreed, _) = prepared(&f.p, request.clone()); + assert_eq!( + semantic_record(&agreed, f.attached[1]), + Some(candidate(&f, 1)) + ); + for keep in [false, true] { + for permute in [false, true] { + let mut review = request.clone(); + if keep { + review.reviews[1].resolutions[0].decision = SemanticDecision::KeepTarget; + } + if permute { + review.reviews.reverse(); + review.transition.sources.reverse(); + review.transition.mappings.reverse(); + } + let plan = plan_semantic_transition(&f.p, review).unwrap(); + if keep { + assert!( + plan.conflicts.iter().any(|e| e.contains("Contradictory")), + "{:?}", + plan.conflicts + ); + } else { + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + assert_eq!( + prepare_semantic_transition(&f.p, &plan).unwrap().project, + agreed + ); + } + } + } + } + } +} diff --git a/crates/oa-core/tests/source6_view_contract.rs b/crates/oa-core/tests/source6_view_contract.rs new file mode 100644 index 00000000..c46de242 --- /dev/null +++ b/crates/oa-core/tests/source6_view_contract.rs @@ -0,0 +1,409 @@ +use oa_core::{source_project::*, transitions::*, view_options::*, *}; +use std::collections::BTreeMap; + +#[test] +fn source6_traceability_default_keeps_both_layers_and_indexed_projection_matches() { + let mut p = Project::empty(); + let oa = p + .add_element(Kind::Activity, Layer::Oa, "OA send", None) + .unwrap(); + let ob = p + .add_element(Kind::Activity, Layer::Oa, "OA receive", None) + .unwrap(); + let sa = p + .add_element(Kind::Function, Layer::Sa, "SA send", None) + .unwrap(); + let sb = p + .add_element(Kind::Function, Layer::Sa, "SA receive", None) + .unwrap(); + let interaction = p.relate(RelationKind::Interaction, "OA report", oa, ob); + let fe = Id::new_v4(); + let request = FunctionalExchangeRequest { + id: fe, + alias: "sa_report".into(), + name: "SA report".into(), + description: String::new(), + source: sa, + target: sb, + source_port: PortChoice::Auto, + target_port: PortChoice::Auto, + items: vec![], + }; + p = execute_batch(&p, &plan_functional_exchange(&p, &request).unwrap()).unwrap(); + let d = p.ensure_diagram(Layer::Oa); + p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability; + for (i, id) in [oa, ob, sa, sb].into_iter().enumerate() { + p.place( + d, + id, + Position { + x: 300. * i as f32, + y: 0., + }, + ); + } + let index = ViewIndex::new(&p); + let view = projection::project_view_with_index(&p, d, &index); + assert!(view.edges.iter().any(|e| e.id == interaction)); + assert!(view.edges.iter().any(|e| e.id == fe)); + assert_eq!(view, projection::project_view(&p, d)); + p.diagrams + .get_mut(&d) + .unwrap() + .view_options + .hidden_edges + .push(fe); + validate_candidate(&p).unwrap(); + assert!( + !projection::project_view_with_index(&p, d, &index) + .edges + .iter() + .any(|e| e.id == fe) + ); + assert!( + projection::project_view_with_index(&p, d, &index) + .edges + .iter() + .any(|e| e.id == interaction) + ); + p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Overview; + assert!(validate_candidate(&p).is_err()); + p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::Traceability; + p.diagrams.get_mut(&d).unwrap().view_options.relation_kinds = RelationKinds::Only(vec![]); + assert!( + projection::project_view_with_index(&p, d, &index) + .edges + .is_empty() + ); +} + +#[test] +fn source6_edge_availability_uses_actual_shared_projection_fold_and_placement_rules() { + let mut p = Project::empty(); + let parent = p + .add_element(Kind::Entity, Layer::Oa, "Team", None) + .unwrap(); + let a = p + .add_element(Kind::Entity, Layer::Oa, "A", Some(parent)) + .unwrap(); + let b = p + .add_element(Kind::Entity, Layer::Oa, "B", Some(parent)) + .unwrap(); + let mean = p.relate( + RelationKind::CommunicationMean, + "Internal communication", + a, + b, + ); + let d = p.ensure_diagram(Layer::Oa); + let outer = p.place(d, parent, Position { x: 0., y: 0. }); + p.place(d, a, Position { x: 20., y: 100. }); + let nb = p.place(d, b, Position { x: 350., y: 100. }); + assert_eq!( + projection::edge_availability(&p, &p.diagrams[&d])[&mean], + EdgeAvailability::Visible + ); + p.diagrams + .get_mut(&d) + .unwrap() + .view_options + .hidden_edges + .push(mean); + assert_eq!( + projection::edge_availability(&p, &p.diagrams[&d])[&mean], + EdgeAvailability::Visible + ); + p.diagrams + .get_mut(&d) + .unwrap() + .nodes + .get_mut(&outer) + .unwrap() + .collapsed = true; + assert_eq!( + projection::edge_availability(&p, &p.diagrams[&d])[&mean], + EdgeAvailability::FoldedInternal + ); + p.diagrams + .get_mut(&d) + .unwrap() + .nodes + .get_mut(&outer) + .unwrap() + .collapsed = false; + p.diagrams.get_mut(&d).unwrap().nodes.remove(&nb); + assert_eq!( + projection::edge_availability(&p, &p.diagrams[&d])[&mean], + EdgeAvailability::EndpointNotShown { + source: false, + target: true + } + ); + p.diagrams.get_mut(&d).unwrap().projection = ProjectionKind::FunctionalDataflow; + assert_eq!( + projection::edge_availability(&p, &p.diagrams[&d])[&mean], + EdgeAvailability::OutsideProjection + ); +} + +fn setup() -> (Project, Id, Id, Id, Id) { + let mut p = Project::empty(); + let a = p.add_element(Kind::Entity, Layer::Oa, "A", None).unwrap(); + let b = p.add_element(Kind::Actor, Layer::Oa, "B", None).unwrap(); + let exchange = p.relate(RelationKind::CommunicationMean, "Communication", a, b); + let category = p + .add_element( + Kind::ExchangeCategory, + Layer::Oa, + "Participant exchanges", + None, + ) + .unwrap(); + *p.elements.get_mut(&category).unwrap().shared_mut().unwrap() = SharedBody::ExchangeCategory { + family: ExchangeFamily::Participant, + exchanges: vec![exchange], + }; + let diagram = p.ensure_diagram(Layer::Oa); + p.place(diagram, a, Position { x: 0., y: 0. }); + p.place(diagram, b, Position { x: 400., y: 0. }); + p.diagrams.get_mut(&diagram).unwrap().view_options = ViewOptions { + participant_categories: CategorySelection::Any(vec![category]), + hidden_edges: vec![exchange], + exchange_labels: ExchangeLabels::NameAndItems, + ..Default::default() + }; + (p, exchange, category, diagram, a) +} + +#[test] +fn source6_views_round_trip_strict_fields_and_reference_aliases() { + let (p, _, category, _, _) = setup(); + validate_candidate(&p).unwrap(); + let source = language::format(&p); + assert!(source.contains("exchange_category")); + assert!(source.contains(&format!("@{}", p.elements[&category].alias))); + assert_eq!(language::parse(&source).unwrap(), p); + assert_eq!( + persistence::decode(&serde_json::to_vec(&p).unwrap()).unwrap(), + p + ); + let bundle = SourceBundle { + entry: "model.arcadia".into(), + files: BTreeMap::from([("model.arcadia".into(), source)]), + }; + assert_eq!( + compile_bundle(&bundle, ProjectLimits::default()) + .unwrap() + .decoded + .project, + p + ); +} + +#[test] +fn source6_view_defaults_cannot_be_smuggled_under_old_json_headers() { + let mut p = Project::empty(); + p.ensure_diagram(Layer::Oa); + let current = serde_json::to_value(&p).unwrap(); + for version in 3..=5 { + let mut old = current.clone(); + old["schema_version"] = version.into(); + for diagram in old["diagrams"].as_object_mut().unwrap().values_mut() { + diagram["view_options"] = serde_json::json!({}); + } + let error = persistence::decode(&serde_json::to_vec(&old).unwrap()) + .unwrap_err() + .to_string(); + assert!( + error.contains("requires schema/source 6"), + "{version}: {error}" + ); + for diagram in old["diagrams"].as_object_mut().unwrap().values_mut() { + diagram.as_object_mut().unwrap().remove("view_options"); + } + assert_eq!( + persistence::decode(&serde_json::to_vec(&old).unwrap()).unwrap(), + p + ); + } +} + +#[test] +fn source6_category_and_saved_selection_reidentify_delete_preserve_empty_filters() { + let (p, exchange, category, diagram, a) = setup(); + let replacement = Id::new_v4(); + let q = execute_batch(&p, &[Command::Reidentify(exchange, replacement)]).unwrap(); + assert!( + matches!(q.elements[&category].shared(),Some(SharedBody::ExchangeCategory{exchanges,..}) if exchanges==&[replacement]) + ); + assert_eq!( + q.diagrams[&diagram].view_options.hidden_edges, + vec![replacement] + ); + let r = execute_batch(&q, &[Command::Delete(replacement)]).unwrap(); + assert!(r.contains(category)); + assert!(r.contains(a)); + assert!(r.diagrams[&diagram].view_options.hidden_edges.is_empty()); + assert!( + matches!(r.elements[&category].shared(),Some(SharedBody::ExchangeCategory{exchanges,..}) if exchanges.is_empty()) + ); + let s = execute_batch(&r, &[Command::Delete(category)]).unwrap(); + assert_eq!( + s.diagrams[&diagram].view_options.participant_categories, + CategorySelection::Any(vec![]) + ); +} + +#[test] +fn source6_category_transition_requires_mapped_members_and_reviews_exact_set() { + let (mut p, exchange, category, _, _) = setup(); + let sa = p + .add_element(Kind::System, Layer::Sa, "System", None) + .unwrap(); + let sb = p + .add_element(Kind::SystemActor, Layer::Sa, "External", None) + .unwrap(); + let port = |p: &mut Project, owner, name: &str| { + let id = Id::new_v4(); + p.elements.insert( + id, + Element { + id, + alias: p.fresh_alias(name, "port", id), + name: name.into(), + description: String::new(), + parent: Some(owner), + body: ElementBody::shared_in( + Layer::Sa, + SharedBody::Port { + contract: PortContract::Component { + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }, + legacy: None, + }, + ), + }, + ); + id + }; + let pa = port(&mut p, sa, "A endpoint"); + let pb = port(&mut p, sb, "B endpoint"); + let boundary = p.relate(RelationKind::BoundaryExchange, "Boundary", pa, pb); + let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![category]); + assert!(!plan_transition(&p, &request).unwrap().conflicts.is_empty()); + request.mappings.push(TransitionMapping { + source: exchange, + target: Some(boundary), + target_kind: None, + }); + let plan = plan_transition(&p, &request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let q = apply_transition(&p, &plan).unwrap(); + let trace = q.traces.values().find(|t| t.oa == category).unwrap(); + let target = trace.sa; + assert!( + matches!(q.elements[&target].shared(),Some(SharedBody::ExchangeCategory{family:ExchangeFamily::Participant,exchanges}) if exchanges==&[boundary]) + ); + let facets = semantic_facets(&q, trace, &BTreeMap::from([(exchange, boundary)])).unwrap(); + assert!( + facets + .iter() + .any(|f| f.selector == FacetSelector::Category(CategoryFacet::Family)) + ); + assert!(facets.iter().any(|f| matches!( + f.selector, + FacetSelector::Member { + collection: ReferenceCollection::CategoryExchanges, + .. + } + ))); +} + +#[test] +fn source6_new_historical_facts_are_rejected_under_source5_even_without_live_categories() { + let mut p = Project::empty(); + let oa = p.add_element(Kind::Entity, Layer::Oa, "OA", None).unwrap(); + let sa = p + .add_element(Kind::SystemActor, Layer::Sa, "SA", None) + .unwrap(); + for fact in [ + FacetReview::Category(CategoryReview::Family(Reviewed { + oa: Observed::NotApplicable, + sa: Observed::NotApplicable, + })), + FacetReview::Member { + collection: ReferenceCollection::CategoryExchanges, + keys: KeyPair { + oa: Some(Id::new_v4()), + sa: None, + }, + review: Reviewed { + oa: Observed::NotApplicable, + sa: Observed::NotApplicable, + }, + }, + ] { + let id = Id::new_v4(); + p.traces.clear(); + p.traces.insert( + id, + Trace { + id, + alias: "review".into(), + name: "History".into(), + description: String::new(), + oa, + sa, + kind: TraceKind::Manual, + baseline: Some(TraceBaseline { + oa: p.properties(oa).unwrap(), + sa: p.properties(sa).unwrap(), + semantic: Some(SemanticBaseline { facets: vec![fact] }), + }), + }, + ); + let mut json = serde_json::to_value(&p).unwrap(); + json["schema_version"] = 5.into(); + let error = persistence::decode(&serde_json::to_vec(&json).unwrap()) + .unwrap_err() + .to_string(); + assert!( + error.contains("category semantic facts require schema/source 6"), + "{error}" + ); + let old = language::format(&p).replacen("arcadia 6;", "arcadia 5;", 1); + assert!(language::parse(&old).unwrap_err().iter().any(|d| { + d.message + .contains("category semantic facts require schema/source 6") + })); + } +} + +#[test] +fn source6_projection_filters_keep_placements_and_label_modes_are_presentation_only() { + let (mut p, exchange, _, diagram, _) = setup(); + let hidden = projection::project_view(&p, diagram); + assert!(hidden.edges.is_empty()); + p.diagrams + .get_mut(&diagram) + .unwrap() + .view_options + .hidden_edges + .clear(); + let shown = projection::project_view(&p, diagram); + assert_eq!(shown.edges.len(), 1); + assert_eq!(shown.edges[0].id, exchange); + assert_eq!(shown.nodes, hidden.nodes); + assert_eq!(shown.edges[0].source_port, None); + assert_eq!(shown.edges[0].target_port, None); + assert!(!shown.edges[0].oriented); + let route = routing::route_diagram(&p, &p.diagrams[&diagram], &shown); + assert!(route.edges[&exchange].label.text.contains("No items")); + p.diagrams + .get_mut(&diagram) + .unwrap() + .view_options + .relation_kinds = RelationKinds::Only(vec![]); + assert!(projection::project_view(&p, diagram).edges.is_empty()); +} diff --git a/crates/oa-core/tests/source_project.rs b/crates/oa-core/tests/source_project.rs index 7c1bc599..43fa42bd 100644 --- a/crates/oa-core/tests/source_project.rs +++ b/crates/oa-core/tests/source_project.rs @@ -157,7 +157,17 @@ fn cross_file_typed_ports_global_forward_references_and_stable_ids() { assert_eq!(again.decoded.project, result.decoded.project); assert_eq!(again.bundle, result.bundle); assert_eq!(again.index, result.index); - assert!(result.decoded.migration.is_none()); + let migration = result.decoded.migration.as_ref().unwrap(); + assert_eq!( + (migration.from_schema, migration.to_schema), + (5, oa_core::SCHEMA_VERSION) + ); + assert!(input.files[&input.entry].contains("arcadia 5;")); + assert!(result.bundle.files[&result.bundle.entry].contains("arcadia 6;")); + assert!( + again.decoded.migration.is_none(), + "stabilized current source must not migrate twice" + ); let project = &result.decoded.project; assert_eq!( project.relations[&id(project, "message")].source(), diff --git a/crates/oa-core/tests/state_diagrams.rs b/crates/oa-core/tests/state_diagrams.rs index a9b89534..959dc940 100644 --- a/crates/oa-core/tests/state_diagrams.rs +++ b/crates/oa-core/tests/state_diagrams.rs @@ -18,6 +18,7 @@ fn view(p: &mut Project, machine: Id, alias: &str) -> Id { description: String::new(), layer: Layer::Oa, projection: ProjectionKind::StateMachine { machine }, + view_options: Default::default(), nodes: BTreeMap::new(), routes: vec![], }, diff --git a/crates/oa-desktop/src/app.rs b/crates/oa-desktop/src/app.rs index acafdf04..971d7569 100644 --- a/crates/oa-desktop/src/app.rs +++ b/crates/oa-desktop/src/app.rs @@ -1,5 +1,13 @@ mod checkpoints; +#[cfg(test)] +mod diagram_selection_tests; mod hosted; +#[cfg(test)] +mod review_navigation_tests; +#[cfg(test)] +mod transition_context_tests; +#[cfg(test)] +mod view_copy_tests; use crate::forms::Draft; use crate::{ document::Document, @@ -288,6 +296,7 @@ pub enum Pending { HostedOpen, New, Demo, + Walkthrough, Open, Close(window::Id), } @@ -346,6 +355,7 @@ pub enum Message { InspectorTab(crate::navigation::InspectorTab), RevealInDiagram(Id, Id), AddDiagram, + DuplicateDiagram(Id), AddArtifact, AddTrace, Field(String, String), @@ -354,6 +364,7 @@ pub enum Message { FieldAppend(String, Value), FieldValuePath(String, crate::data_values::FieldPathKey), FieldValueBrowse(String, Option, usize), + FieldReferenceBrowse(String, String, usize), FieldVariant(String, Value), FieldArray(String, Option, i32), Apply, @@ -897,6 +908,44 @@ impl App { 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; @@ -1530,7 +1579,14 @@ impl App { self.canvas_epoch = self.canvas_epoch.wrapping_add(1); self.selected_bend = None; } - Message::Guide(event) => self.guide.update(event), + 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) { @@ -1575,6 +1631,22 @@ impl App { .insert(path, crate::forms::ValueBrowser { scope, page }); } } + 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) @@ -1799,13 +1871,11 @@ impl App { match state.command(self.project()) { Ok(command) => { if self.execute(command) { - if let Some(target) = focus { - self.selected = target; - if let Some(layer) = self.project().layer_of(target) - { - self.layer = layer; - } - self.refresh_draft(); + 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(); @@ -1827,11 +1897,19 @@ impl App { && 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.plan.changes.first().map(|c| c.target) - } + 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(); match workflow.update(self.project(), event) { Ok(Some(mut candidate)) => { let focus = transition_focus @@ -1849,25 +1927,33 @@ impl App { .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| { preferred_diagram(&candidate, self.active_diagram, layer, id) }) }); - if let (Some(id), Some(diagram)) = (focus, destination) { + 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.selected = id; - if let Some(layer) = self.project().layer_of(id) { - self.layer = layer; - self.active_diagram = - destination.or_else(|| self.diagram_for(layer, id)); - } - self.refresh_draft(); + self.focus_after_review(id, destination); } - if !stay_in_catalog { + 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(); } @@ -2105,6 +2191,7 @@ impl App { layer: self.layer, projection: ProjectionKind::Allocation, routes: Vec::new(), + view_options: Default::default(), nodes: Default::default(), }; if self.execute(Command::CreateDiagram(d)) { @@ -2113,6 +2200,39 @@ impl App { } } } + 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(); @@ -2206,11 +2326,28 @@ impl App { }); } 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() { - self.active_diagram = Some(choice.id); - if let Some(d) = self.project().diagrams.get(&choice.id) { + // 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(); @@ -2841,6 +2978,7 @@ impl App { layer: self.layer, projection: preferred, routes: Vec::new(), + view_options: Default::default(), nodes: Default::default(), })); diagram_target = Some(view); @@ -3009,6 +3147,13 @@ impl App { 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(); @@ -4440,6 +4585,7 @@ mod port_view_tests { layer: Layer::Sa, projection: ProjectionKind::Capabilities, routes: Vec::new(), + view_options: Default::default(), nodes: Default::default(), }, ); diff --git a/crates/oa-desktop/src/app/checkpoints.rs b/crates/oa-desktop/src/app/checkpoints.rs index 73b39ac0..8faf6037 100644 --- a/crates/oa-desktop/src/app/checkpoints.rs +++ b/crates/oa-desktop/src/app/checkpoints.rs @@ -16,6 +16,7 @@ impl App { home::Event::New => self.handle(Message::Request(Pending::New)), home::Event::Open => self.handle(Message::Request(Pending::Open)), home::Event::Example => self.handle(Message::Request(Pending::Demo)), + home::Event::Walkthrough => self.handle(Message::Request(Pending::Walkthrough)), home::Event::Guide => self.update(Message::Page(Page::Guide)), home::Event::Resume => self.update(Message::Page(self.last_workspace_page)), home::Event::OpenRecent(path) => { @@ -151,6 +152,7 @@ impl App { self.reset(restored.document.project().clone(), None); self.hosted.leave_document(); self.document = restored.document; + self.migration = self.document.compiled().decoded.migration.clone(); self.selected = if self.project().layer_of(restored.selected).is_some() || restored.selected == self.project().id { @@ -184,7 +186,11 @@ impl App { } else { Page::Diagram }; - self.status="Recovered an unsaved local copy. Choose Save as to save it; the original file and hosted model are unchanged.".into(); + self.status = if self.migration.is_some() { + "Recovered source 5 as source 6 in an unsaved local copy. Draft text is preserved; workflow previews were rebuilt and semantic impact approvals need review. Choose Save as to save it." + } else { + "Recovered an unsaved local copy. Choose Save as to save it; the original file and hosted model are unchanged." + }.into(); self.request_lint() } } @@ -279,6 +285,28 @@ mod tests { assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); } + #[test] + fn walkthrough_request_keeps_unfinished_source_behind_the_normal_pending_guard() { + let mut app = App::with_project(Project::demo()); + let accepted = app.document.snapshot(); + let raw = format!( + "{}\nunfinished walkthrough draft", + app.source_workspace.text() + ); + app.source_workspace + .edit(raw.clone(), app.source_editor.cursor()); + let task = app.update(Message::Home(home::Event::Walkthrough)); + assert!(matches!(app.pending, Some(Pending::Walkthrough))); + assert!(!app.busy); + assert!(iced_runtime::task::into_stream(task).is_none()); + assert_eq!(app.source_workspace.text(), raw); + assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); + let _ = app.update(Message::Escape); + assert!(app.pending.is_none()); + assert_eq!(app.source_workspace.text(), raw); + assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); + } + fn retained_snapshot(app: &App) -> recovery::Snapshot { recovery::Snapshot::capture( app.canvas_document, diff --git a/crates/oa-desktop/src/app/diagram_selection_tests.rs b/crates/oa-desktop/src/app/diagram_selection_tests.rs new file mode 100644 index 00000000..0a4a87fb --- /dev/null +++ b/crates/oa-desktop/src/app/diagram_selection_tests.rs @@ -0,0 +1,150 @@ +//! A view picker follows a view inspector without stealing semantic selection. +use super::*; + +fn fixture() -> (App, Id, Id, Id) { + let project = language::parse( + r#"arcadia 6; project views "View switching" { + oa_activity work "Inspect work" {} + diagram first "Original concern" {} + diagram second "Commands review" {} + node original_work { diagram @first; element @work; } + node copied_work { diagram @second; element @work; } + }"#, + ) + .unwrap(); + let id = |alias| { + project + .identities() + .into_iter() + .find(|(_, value)| *value == alias) + .unwrap() + .0 + }; + let (first, second, work) = (id("first"), id("second"), id("work")); + let mut app = App::with_project(project); + app.page = Page::Diagram; + app.active_diagram = Some(second); + app.selected = second; + app.refresh_draft(); + (app, first, second, work) +} +fn choose(app: &mut App, id: Id) { + let _ = app.update(Message::Diagram(Choice { + id, + label: "Captured choice".into(), + })); +} +fn field(app: &mut App, path: &str, value: &str) { + let _ = app.update(Message::Field(path.into(), value.into())); +} + +#[test] +fn changing_visible_view_follows_its_clean_inspector_without_a_model_edit() { + let (mut app, original, copy, _) = fixture(); + let before = app.document.snapshot(); + let revision = app.document.revision(); + choose(&mut app, original); + assert_eq!(app.active_diagram, Some(original)); + assert_eq!(app.selected, original); + assert_eq!(app.draft.as_ref().unwrap().original, original); + assert_eq!( + app.draft.as_ref().unwrap().inputs["/name"].raw, + "Original concern" + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert_eq!(app.document.revision(), revision); + assert!(!app.document.can_undo()); + field(&mut app, "/name", "Edited visible original"); + assert!(app.apply()); + assert_eq!( + app.project().diagrams[&original].name, + "Edited visible original" + ); + assert_eq!(app.project().diagrams[©].name, "Commands review"); +} + +#[test] +fn invalid_view_draft_blocks_the_switch_and_retains_raw_input() { + let (mut app, original, copy, _) = fixture(); + field(&mut app, "/id", "-"); + let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + let before = app.document.snapshot(); + choose(&mut app, original); + assert_eq!(app.active_diagram, Some(copy)); + assert_eq!(app.selected, copy); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + raw + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); +} + +#[test] +fn valid_pending_view_edit_applies_to_old_view_then_inspects_the_new_view() { + let (mut app, original, copy, _) = fixture(); + let before = app.document.snapshot(); + field(&mut app, "/name", "Reviewed copy"); + choose(&mut app, original); + assert_eq!(app.project().diagrams[©].name, "Reviewed copy"); + assert_eq!( + app.project().diagrams[&original], + before.decoded.project.diagrams[&original] + ); + assert_eq!(app.selected, original); + assert_eq!(app.draft.as_ref().unwrap().original, original); + let _ = app.update(Message::Undo); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); +} + +#[test] +fn diagram_uuid_edit_follows_the_same_view_or_a_different_picker_target() { + for same_view in [false, true] { + let (mut app, original, copy, _) = fixture(); + let changed = Id::from_u128(0xabcd123440008000000000000001); + field(&mut app, "/id", &changed.to_string()); + choose(&mut app, if same_view { copy } else { original }); + let expected = if same_view { changed } else { original }; + assert!(!app.project().diagrams.contains_key(©)); + assert!(app.project().diagrams.contains_key(&changed)); + assert_eq!(app.active_diagram, Some(expected)); + assert_eq!(app.selected, expected); + assert_eq!(app.draft.as_ref().unwrap().original, expected); + assert!(!app.draft_dirty()); + } +} + +#[test] +fn diagram_switch_preserves_an_independent_semantic_inspection() { + let (mut app, original, _, work) = fixture(); + let _ = app.update(Message::Select(work)); + let before = app.document.snapshot(); + choose(&mut app, original); + assert_eq!(app.active_diagram, Some(original)); + assert_eq!(app.selected, work); + assert_eq!(app.draft.as_ref().unwrap().original, work); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); +} + +#[test] +fn stale_diagram_choice_never_applies_an_unrelated_valid_draft() { + let (mut app, _, copy, _) = fixture(); + field(&mut app, "/name", "Pending copy name"); + let before = app.document.snapshot(); + choose(&mut app, Id::new_v4()); + assert_eq!(app.active_diagram, Some(copy)); + assert_eq!(app.selected, copy); + assert_eq!( + app.draft.as_ref().unwrap().inputs["/name"].raw, + "Pending copy name" + ); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + assert!(app.status.contains("no longer exists")); +} diff --git a/crates/oa-desktop/src/app/review_navigation_tests.rs b/crates/oa-desktop/src/app/review_navigation_tests.rs new file mode 100644 index 00000000..741d1b36 --- /dev/null +++ b/crates/oa-desktop/src/app/review_navigation_tests.rs @@ -0,0 +1,184 @@ +//! Accepted semantic review keeps selection, layer and visible diagram coherent. +use super::*; +use crate::{ + semantic_review::{RowEvent, State}, + workflow::{Event, Workflow}, +}; + +fn send(app: &mut App, message: Message) { + let _ = app.update(message); +} +fn state(app: &App) -> &State { + let Some(Workflow::Semantic(state)) = &app.workflow else { + panic!("{}", app.status) + }; + state +} +fn row_event(app: &mut App, event: RowEvent) { + let event = state(app).address_event(event); + send(app, Message::Workflow(Event::Semantic(event))); +} +fn fixture(reverse: bool, destination_view: bool) -> (App, Id, Id, Id, Option) { + let mut p = Project::empty(); + let oa = p + .add_element(Kind::Activity, Layer::Oa, "Confirm stable pressure", None) + .unwrap(); + let sa = p + .add_element(Kind::Function, Layer::Sa, "Confirm safe pressure", None) + .unwrap(); + let trace = Id::new_v4(); + p.traces.insert( + trace, + Trace { + id: trace, + alias: "confirm_trace".into(), + name: "Confirmation refinement".into(), + description: String::new(), + oa, + sa, + kind: TraceKind::Refinement, + baseline: None, + }, + ); + let (source, target, source_layer, target_layer) = if reverse { + (sa, oa, Layer::Sa, Layer::Oa) + } else { + (oa, sa, Layer::Oa, Layer::Sa) + }; + let from = p.ensure_diagram(source_layer); + p.place(from, source, Position { x: 91., y: 143. }); + let to = destination_view.then(|| { + let view = p.ensure_diagram(target_layer); + p.place(view, target, Position { x: 487., y: 213. }); + view + }); + let mut app = App::with_project(p); + app.page = Page::Diagram; + app.layer = source_layer; + app.active_diagram = Some(from); + app.selected = source; + app.refresh_draft(); + (app, source, target, from, to) +} +fn name_take(app: &mut App, source: Id) -> Project { + send(app, Message::StartSemanticReview(source)); + let facet = FacetSelector::Header(HeaderFacet::Name); + row_event(app, RowEvent::Select(facet.clone())); + row_event( + app, + RowEvent::Decide(facet, Some(SemanticDecision::TakeSource)), + ); + assert!(state(app).can_apply(), "{:?}", state(app).error); + let Some(Workflow::Semantic(state)) = &mut app.workflow else { + unreachable!() + }; + let command = state.command(app.document.project()).unwrap(); + execute_batch(app.document.project(), &[command]).unwrap() +} + +#[test] +fn semantic_name_take_selects_the_counterpart_view_in_both_directions_with_one_exact_history_entry() +{ + for reverse in [false, true] { + let (mut app, source, target, from, to) = fixture(reverse, true); + app.search = format!("@{}", app.project().elements[&source].alias); + let query = app.search.clone(); + let before = app.document.snapshot(); + let expected = name_take(&mut app, source); + app.selected_node = app.project().diagrams[&from].nodes.keys().next().copied(); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.project(), &expected); + assert_eq!(app.project().diagrams, before.decoded.project.diagrams); + assert_eq!(app.selected, target); + assert_eq!(app.layer, app.project().elements[&target].layer()); + assert_eq!(app.active_diagram, to); + assert_eq!( + app.project().diagrams[&app.active_diagram.unwrap()].layer, + app.layer + ); + // refresh_draft deliberately selects the target's visible placement. + // The invariant is that the old-layer placement cannot survive. + let placement = app.selected_node.expect("visible counterpart placement"); + assert_eq!( + app.project().diagrams[&to.unwrap()].nodes[&placement].element, + target + ); + assert!(!app.project().diagrams[&from].nodes.contains_key(&placement)); + assert_eq!(app.page, Page::Diagram); + assert_eq!(app.search, query); + assert!(app.workflow.is_none()); + assert_eq!(app.document.revision(), 1); + let accepted = app.document.snapshot(); + assert_eq!( + source_project::compile_bundle(&accepted.bundle, LIMITS) + .unwrap() + .decoded + .project, + expected + ); + send(&mut app, Message::Undo); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + send(&mut app, Message::Redo); + assert_eq!(app.project(), &accepted.decoded.project); + assert_eq!(app.document.bundle(), &accepted.bundle); + } +} + +#[test] +fn semantic_review_without_a_destination_view_clears_the_old_layer_canvas_without_creating_a_view() +{ + let (mut app, source, target, _, _) = fixture(false, false); + let expected = name_take(&mut app, source); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.project(), &expected); + assert_eq!(app.selected, target); + assert_eq!(app.layer, Layer::Sa); + assert_eq!(app.active_diagram, None); + assert_eq!(app.project().diagrams.len(), 1); +} + +#[test] +fn review_already_showing_the_target_keeps_its_camera_and_query() { + let (mut app, source, target, _, to) = fixture(false, true); + app.layer = Layer::Sa; + app.active_diagram = to; + app.selected = target; + app.zoom = 1.31; + app.pan = iced::Vector::new(-219., 88.); + app.search = "pressure".into(); + app.refresh_draft(); + let expected = name_take(&mut app, source); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.project(), &expected); + assert_eq!(app.active_diagram, to); + assert_eq!(app.zoom, 1.31); + assert_eq!(app.pan, iced::Vector::new(-219., 88.)); + assert_eq!(app.search, "pressure"); +} + +#[test] +fn rejected_semantic_source_writeback_retains_review_camera_and_original_navigation() { + let (mut app, source, _, from, _) = fixture(false, true); + let bundle = app.document.bundle().clone(); + let mut limits = source_project::ProjectLimits::default(); + limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::() + 1; + app.document = Document::from_bundle(bundle, limits).unwrap(); + app.zoom = 1.21; + app.pan = iced::Vector::new(72., -44.); + app.search = "confirmation".into(); + let before = app.document.snapshot(); + let _valid_model = name_take(&mut app, source); + send(&mut app, Message::Workflow(Event::Apply)); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + assert_eq!(app.selected, source); + assert_eq!(app.layer, Layer::Oa); + assert_eq!(app.active_diagram, Some(from)); + assert_eq!(app.page, Page::Diagram); + assert_eq!(app.zoom, 1.21); + assert_eq!(app.pan, iced::Vector::new(72., -44.)); + assert_eq!(app.search, "confirmation"); + assert!(state(&app).error.is_some()); + assert!(!app.document.can_undo()); +} diff --git a/crates/oa-desktop/src/app/transition_context_tests.rs b/crates/oa-desktop/src/app/transition_context_tests.rs new file mode 100644 index 00000000..65425248 --- /dev/null +++ b/crates/oa-desktop/src/app/transition_context_tests.rs @@ -0,0 +1,298 @@ +//! Transition navigation must not add presentation edits to a behavior review. +use super::*; +use crate::workflow::{Event, Workflow}; +use oa_core::transitions::*; + +fn send(app: &mut App, message: Message) { + let _ = app.update(message); +} + +fn element( + p: &mut Project, + number: u128, + kind: Kind, + layer: Layer, + name: &str, + parent: Option, +) -> Id { + let mut e = p.make_element(kind, layer, name, parent).unwrap(); + e.id = Id::from_u128(number); + let id = e.id; + p.elements.insert(id, e); + id +} + +fn fixture(layer: Layer, kind: Kind) -> (App, Id, Id, Id) { + let mut p = Project::empty(); + let other = if layer == Layer::Oa { + Layer::Sa + } else { + Layer::Oa + }; + let work_kind = |l| { + if l == Layer::Oa { + Kind::Activity + } else { + Kind::Function + } + }; + let work = element(&mut p, 1, work_kind(layer), layer, "Assess pressure", None); + let reused = element(&mut p, 10, work_kind(other), other, "Assess pressure", None); + let root = element(&mut p, 100, kind, layer, "Restore service", None); + if kind == Kind::Scenario { + let line = element(&mut p, 2, Kind::Lifeline, layer, "Assessment", Some(root)); + *p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(work), + }; + } + let view = p.ensure_diagram(other); + p.diagrams.get_mut(&view).unwrap().name = "Unrelated architecture".into(); + p.place(view, reused, Position { x: 873., y: 421. }); + let mut app = App::with_project(p); + app.layer = layer; + app.page = Page::Behavior; + app.selected = root; + app.behavior.active_owner = Some(root); + app.refresh_draft(); + (app, root, work, reused) +} + +fn review( + app: &mut App, + selected: Id, + mapping: Option<(Id, Id)>, + destination: Option, +) -> (Id, Project) { + app.selected = selected; + app.refresh_draft(); + send(app, Message::StartTransition); + let Some(Workflow::Transition(t)) = &mut app.workflow else { + panic!("{}", app.status) + }; + t.request.scope = DependencyScope::Subtree; + t.request.destination_diagram = destination; + if let Some((source, target)) = mapping { + t.request.mappings.push(TransitionMapping { + source, + target: Some(target), + target_kind: None, + }); + } + t.plan = plan_transition(app.document.project(), &t.request).unwrap(); + assert!(t.plan.conflicts.is_empty(), "{:?}", t.plan.conflicts); + let target = t + .plan + .changes + .iter() + .find(|c| c.source == selected) + .unwrap() + .target; + let expected = apply_transition(app.document.project(), &t.plan).unwrap(); + (target, expected) +} + +#[test] +fn scenario_transition_focuses_requested_counterpart_and_preserves_unrelated_views_in_both_directions() + { + for layer in [Layer::Oa, Layer::Sa] { + let (mut app, root, work, reused) = fixture(layer, Kind::Scenario); + let stale_selection = crate::behavior::scenario::CanvasEvent::Select( + app.scenario_context().unwrap(), + Id::from_u128(2), + ); + let before = app.document.snapshot(); + let (target, expected) = review(&mut app, root, Some((work, reused)), None); + let Some(Workflow::Transition(t)) = &app.workflow else { + unreachable!() + }; + assert_ne!( + t.plan.changes.first().unwrap().source, + root, + "Fixture must expose a dependency before the selected root" + ); + app.selected_node = Some(Id::from_u128(999)); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!( + app.project(), + &expected, + "Desktop must preserve the core candidate exactly" + ); + assert_eq!(app.project().diagrams, before.decoded.project.diagrams); + assert_eq!(app.page, Page::Behavior); + assert_eq!(app.selected, target); + assert_eq!(app.behavior.active_owner, Some(target)); + assert_eq!(app.scenario_context().unwrap().scenario, target); + assert_eq!(app.selected_node, None); + assert!(app.workflow.is_none()); + assert_eq!(app.document.revision(), 1); + let accepted = app.document.snapshot(); + send(&mut app, Message::ScenarioCanvas(stale_selection.clone())); + assert_eq!(app.selected, target); + assert!(Arc::ptr_eq(&accepted, &app.document.snapshot())); + assert_eq!( + source_project::compile_bundle(&accepted.bundle, LIMITS) + .unwrap() + .decoded + .project, + expected + ); + send(&mut app, Message::Undo); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + assert!(!app.document.can_undo()); + let undone_selection = app.selected; + send(&mut app, Message::ScenarioCanvas(stale_selection)); + assert_eq!(app.selected, undone_selection); + assert_eq!(app.project(), &before.decoded.project); + send(&mut app, Message::Redo); + assert_eq!(app.project(), &accepted.decoded.project); + assert_eq!(app.document.bundle(), &accepted.bundle); + } +} + +#[test] +fn explicit_behavior_internal_and_process_roots_keep_their_workspace_without_creating_a_view() { + for (layer, kind) in [ + (Layer::Oa, Kind::Scenario), + (Layer::Sa, Kind::Scenario), + (Layer::Oa, Kind::Process), + (Layer::Sa, Kind::FunctionalChain), + ] { + let (mut app, root, work, reused) = fixture(layer, kind); + let mut p = app.project().clone(); + p.diagrams.clear(); + app = App::with_project(p); + app.page = Page::Behavior; + let selected = if kind == Kind::Scenario { + Id::from_u128(2) + } else { + root + }; + let (target, expected) = review(&mut app, selected, Some((work, reused)), None); + let owner = crate::behavior::selected_behavior(&expected, target).unwrap(); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.project(), &expected); + assert!(app.project().diagrams.is_empty()); + assert_eq!(app.selected, target); + assert_eq!(app.behavior.active_owner, Some(owner)); + assert_eq!(app.page, Page::Behavior); + } +} + +#[test] +fn explicit_destination_is_honored_and_ordinary_work_transition_still_places_in_architecture() { + let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario); + let destination = *app.project().diagrams.keys().next().unwrap(); + let (target, expected) = review(&mut app, root, Some((work, reused)), Some(destination)); + assert!( + expected.diagrams[&destination] + .nodes + .values() + .any(|n| n.element == target) + ); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.project(), &expected); + assert_eq!(app.page, Page::Behavior); + let (mut app, _, work, _) = fixture(Layer::Oa, Kind::Scenario); + let (target, _) = review(&mut app, work, None, None); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.page, Page::Diagram); + assert_eq!(app.selected, target); + assert!( + app.project().diagrams[&app.active_diagram.unwrap()] + .nodes + .values() + .any(|n| n.element == target) + ); +} + +#[test] +fn machine_and_typed_action_transitions_resolve_the_owner_and_reusing_that_context_retains_camera() +{ + for selected_action in [false, true] { + let mut p = Project::empty(); + let owner = element(&mut p, 1, Kind::Entity, Layer::Oa, "Control room", None); + let counterpart = element(&mut p, 10, Kind::System, Layer::Sa, "Control room", None); + let machine = element( + &mut p, + 100, + Kind::StateMachine, + Layer::Oa, + "Pressure modes", + Some(owner), + ); + let region = element(&mut p, 101, Kind::Region, Layer::Oa, "Main", Some(machine)); + let state = element(&mut p, 102, Kind::State, Layer::Oa, "Waiting", Some(region)); + let work = element(&mut p, 2, Kind::Activity, Layer::Oa, "Assess alarm", None); + let action = Id::from_u128(103); + p.relations.insert( + action, + Relation { + id: action, + alias: "assess_entry".into(), + name: "Assess on entry".into(), + description: String::new(), + body: RelationBody::StateAction { + source: state, + target: work, + phase: StateActionKind::Entry, + }, + }, + ); + let mut app = App::with_project(p); + app.page = Page::Behavior; + app.behavior.active_owner = Some(machine); + let selected = if selected_action { action } else { machine }; + let (target, expected) = review(&mut app, selected, Some((owner, counterpart)), None); + let target_machine = crate::behavior::selected_behavior(&expected, target).unwrap(); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.project(), &expected); + assert_eq!(app.page, Page::Behavior); + assert_eq!(app.selected, target); + assert_eq!(app.behavior.active_owner, Some(target_machine)); + assert!( + app.project().diagrams.is_empty(), + "The state workspace must offer explicit view creation" + ); + app.behavior.diagram.zoom = 1.17; + app.behavior.diagram.pan = iced::Vector::new(-73., 146.); + let epoch = app.canvas_epoch; + let (same_target, _) = review(&mut app, selected, Some((owner, counterpart)), None); + assert_eq!(same_target, target); + send(&mut app, Message::Workflow(Event::Apply)); + assert_eq!(app.behavior.diagram.zoom, 1.17); + assert_eq!(app.behavior.diagram.pan, iced::Vector::new(-73., 146.)); + assert_eq!(app.canvas_epoch, epoch); + } +} + +#[test] +fn rejected_source_writeback_keeps_review_and_navigation_and_invalid_draft_still_blocks_start() { + let (mut app, root, work, reused) = fixture(Layer::Oa, Kind::Scenario); + 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::() + 1; + app.document = Document::from_bundle(bundle.clone(), limits).unwrap(); + let (_, _) = review(&mut app, root, Some((work, reused)), None); + let before = app.document.snapshot(); + send(&mut app, Message::Workflow(Event::Apply)); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); + assert!(app.workflow.is_some()); + assert_eq!(app.page, Page::Behavior); + assert_eq!(app.selected, root); + assert_eq!(app.behavior.active_owner, Some(root)); + assert!(!app.document.can_undo()); + app.workflow = None; + send( + &mut app, + Message::Field("/alias".into(), "not an alias!".into()), + ); + let draft = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(); + send(&mut app, Message::StartTransition); + assert!(app.workflow.is_none()); + assert_eq!( + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(), + draft + ); + assert!(Arc::ptr_eq(&before, &app.document.snapshot())); +} diff --git a/crates/oa-desktop/src/app/view_copy_tests.rs b/crates/oa-desktop/src/app/view_copy_tests.rs new file mode 100644 index 00000000..8ffd0a1c --- /dev/null +++ b/crates/oa-desktop/src/app/view_copy_tests.rs @@ -0,0 +1,222 @@ +//! App-level copy behavior, with separate state/architecture view identities. +use super::*; + +struct Fixture { + app: App, + architecture: Id, + states: Id, + machine: Id, + region: Id, + state: Id, +} + +fn fixture() -> Fixture { + let project = language::parse( + r#"arcadia 3; project copied_views "Copied views" { + oa_entity crew "Crew" {} + oa_state_machine modes "Modes" { parent @crew; } + oa_region main "Main" { parent @modes; } + oa_state ready "Ready" { parent @main; } + diagram architecture "Architecture" {} + diagram state_view "Modes view" { projection { StateMachine { machine @modes; }; }; } + node crew_view { diagram @architecture; element @crew; } + node main_view { diagram @state_view; element @main; } + node ready_view { diagram @state_view; element @ready; } + }"#, + ) + .unwrap(); + let identity = |alias| { + project + .identities() + .into_iter() + .find(|(_, candidate)| *candidate == alias) + .unwrap() + .0 + }; + let architecture = identity("architecture"); + let states = identity("state_view"); + let machine = identity("modes"); + let region = identity("main"); + let state = identity("ready"); + let mut app = App::with_project(project); + // Initial view UUID order must not determine the test's workspace. + app.page = Page::Behavior; + app.active_diagram = Some(architecture); + app.behavior.active_owner = Some(machine); + app.behavior.diagram.diagram = Some(states); + app.selected = state; + app.refresh_draft(); + Fixture { + app, + architecture, + states, + machine, + region, + state, + } +} + +fn send(app: &mut App, message: Message) { + let _ = app.update(message); +} + +fn raw_property(app: &App) -> serde_json::Value { + serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap() +} + +#[test] +fn copying_the_current_state_view_uses_its_own_identity_and_is_one_source_undo() { + let mut f = fixture(); + assert_eq!(f.app.state_context().unwrap().diagram, f.states); + assert_eq!(f.app.active_diagram, Some(f.architecture)); + let before = f.app.document.snapshot(); + let revision = f.app.document.revision(); + send(&mut f.app, Message::DuplicateDiagram(f.states)); + assert_eq!(f.app.page, Page::Behavior); + assert_eq!(f.app.behavior.active_owner, Some(f.machine)); + let copied = f.app.state_context().unwrap().diagram; + assert_ne!(copied, f.states); + assert_ne!(copied, f.architecture); + assert_eq!(f.app.active_diagram, Some(copied)); + assert_eq!(f.app.selected, copied); + assert_eq!( + f.app.project().diagrams[&copied].projection.state_machine(), + Some(f.machine) + ); + assert_eq!( + f.app.project().diagrams[&copied] + .nodes + .values() + .map(|node| node.element) + .collect::>(), + BTreeSet::from([f.region, f.state]) + ); + assert_eq!(f.app.document.revision(), revision + 1); + let accepted = f.app.document.snapshot(); + assert_eq!( + source_project::compile_bundle(f.app.document.bundle(), LIMITS) + .unwrap() + .decoded + .project, + *f.app.project() + ); + let mut only_original = f.app.project().clone(); + only_original.diagrams.remove(&copied); + assert_eq!(only_original, before.decoded.project); + send(&mut f.app, Message::Undo); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert!(!f.app.document.can_undo()); + send(&mut f.app, Message::Redo); + assert_eq!(f.app.project(), &accepted.decoded.project); + assert_eq!(f.app.document.bundle(), &accepted.bundle); +} + +#[test] +fn copying_architecture_keeps_architecture_even_with_a_retained_state_view() { + let mut f = fixture(); + f.app.page = Page::Diagram; + f.app.active_diagram = Some(f.architecture); + f.app.selected = f.architecture; + f.app.refresh_draft(); + send(&mut f.app, Message::DuplicateDiagram(f.architecture)); + let copied = f.app.active_diagram.unwrap(); + assert_ne!(copied, f.architecture); + assert_eq!(f.app.page, Page::Diagram); + assert!(f.app.state_context().is_none()); + assert_eq!(f.app.project().diagrams[&copied].name, "Architecture copy"); + assert_eq!( + f.app.project().diagrams[&copied].projection, + f.app.project().diagrams[&f.architecture].projection + ); + assert_eq!(f.app.project().diagrams[&copied].nodes.len(), 1); + assert_eq!(f.app.selected, copied); +} + +#[test] +fn stale_copy_does_not_apply_an_unrelated_valid_property_draft() { + let mut f = fixture(); + send( + &mut f.app, + Message::Field("/name".into(), "Pending state rename".into()), + ); + let raw = raw_property(&f.app); + let before = f.app.document.snapshot(); + let revision = f.app.document.revision(); + send(&mut f.app, Message::DuplicateDiagram(Id::new_v4())); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.document.revision(), revision); + assert_eq!(raw_property(&f.app), raw); + assert_eq!(f.app.selected, f.state); + assert!(!f.app.document.can_undo()); + assert!(f.app.status.contains("no longer exists")); +} + +#[test] +fn duplicate_rejection_preserves_invalid_property_and_pending_behavior_drafts() { + let mut f = fixture(); + send( + &mut f.app, + Message::Field("/alias".into(), "invalid alias!".into()), + ); + let before = f.app.document.snapshot(); + let raw = raw_property(&f.app); + send(&mut f.app, Message::DuplicateDiagram(f.states)); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(raw_property(&f.app), raw); + assert!(!f.app.document.can_undo()); + + let mut f = fixture(); + send( + &mut f.app, + Message::Behavior(crate::behavior::Event::States( + crate::behavior::states::Event::Start( + f.machine, + crate::behavior::states::Tool::Vertex, + f.region, + ), + )), + ); + send( + &mut f.app, + Message::Behavior(crate::behavior::Event::States( + crate::behavior::states::Event::Name("Unfinished nested state".into()), + )), + ); + assert!(f.app.behavior.has_pending()); + let raw = f.app.behavior.recovery_snapshot().unwrap(); + let before = f.app.document.snapshot(); + send(&mut f.app, Message::DuplicateDiagram(f.states)); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.behavior.recovery_snapshot().unwrap(), raw); + assert_eq!(f.app.state_context().unwrap().diagram, f.states); + assert!(!f.app.document.can_undo()); +} + +#[test] +fn duplicate_rejection_preserves_an_incomplete_source_buffer() { + let mut f = fixture(); + let raw = "arcadia 6; project unfinished \"Unfinished source\" {".to_owned(); + send( + &mut f.app, + Message::SourceEdit(text_editor::Action::SelectAll), + ); + send( + &mut f.app, + Message::SourceEdit(text_editor::Action::Edit(text_editor::Edit::Paste( + Arc::new(raw.clone()), + ))), + ); + assert!(f.app.source_dirty()); + let before = f.app.document.snapshot(); + let source = f.app.source_workspace.bundle(); + send(&mut f.app, Message::DuplicateDiagram(f.states)); + assert_eq!(f.app.project(), &before.decoded.project); + assert_eq!(f.app.document.bundle(), &before.bundle); + assert_eq!(f.app.source_workspace.bundle(), source); + assert_eq!(f.app.source_workspace.text(), raw); + assert!(!f.app.document.can_undo()); +} diff --git a/crates/oa-desktop/src/behavior.rs b/crates/oa-desktop/src/behavior.rs index 8f63e764..c1806f00 100644 --- a/crates/oa-desktop/src/behavior.rs +++ b/crates/oa-desktop/src/behavior.rs @@ -773,7 +773,9 @@ pub fn selected_behavior(p: &Project, selected: Id) -> Option { .filter(|r| { matches!( r.kind(), - RelationKind::StateTransition | RelationKind::SequenceLink + RelationKind::StateTransition + | RelationKind::SequenceLink + | RelationKind::StateAction ) }) .map(|r| r.source()) diff --git a/crates/oa-desktop/src/behavior/scenario.rs b/crates/oa-desktop/src/behavior/scenario.rs index 5a0ff1cc..37e4894d 100644 --- a/crates/oa-desktop/src/behavior/scenario.rs +++ b/crates/oa-desktop/src/behavior/scenario.rs @@ -669,6 +669,7 @@ fn exchanges(p: &Project, d: &Draft) -> Vec { && matches!( r.kind(), RelationKind::Interaction + | RelationKind::CommunicationMean | RelationKind::FunctionalExchange | RelationKind::BoundaryExchange ) @@ -677,7 +678,9 @@ fn exchanges(p: &Project, d: &Draft) -> Vec { ScenarioViewpoint::Functional => r.kind() == RelationKind::FunctionalExchange, ScenarioViewpoint::ParticipantInteraction => matches!( r.kind(), - RelationKind::Interaction | RelationKind::BoundaryExchange + RelationKind::Interaction + | RelationKind::CommunicationMean + | RelationKind::BoundaryExchange ), ScenarioViewpoint::BoundaryInterface => matches!( r.kind(), @@ -687,17 +690,37 @@ fn exchanges(p: &Project, d: &Draft) -> Vec { } && (!d.fixed_endpoints || d.sender.zip(d.receiver).is_some_and(|(sender, receiver)| { - validation::lifeline_matches_endpoint(p, sender, r.source()) == Some(true) - && validation::lifeline_matches_endpoint(p, receiver, r.target()) - == Some(true) + 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()) ), }) @@ -837,6 +860,38 @@ impl Draft { 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) }) @@ -2315,6 +2370,81 @@ pub(crate) mod direct_tests { #[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) diff --git a/crates/oa-desktop/src/behavior/state_canvas.rs b/crates/oa-desktop/src/behavior/state_canvas.rs index 4565679c..145aad92 100644 --- a/crates/oa-desktop/src/behavior/state_canvas.rs +++ b/crates/oa-desktop/src/behavior/state_canvas.rs @@ -1538,6 +1538,7 @@ mod tests { projection: ProjectionKind::StateMachine { machine }, nodes: BTreeMap::new(), routes: vec![], + view_options: Default::default(), }, ); for (element, x, y, width, height) in [ diff --git a/crates/oa-desktop/src/behavior/state_edit.rs b/crates/oa-desktop/src/behavior/state_edit.rs index 173c53aa..9aae33cb 100644 --- a/crates/oa-desktop/src/behavior/state_edit.rs +++ b/crates/oa-desktop/src/behavior/state_edit.rs @@ -293,6 +293,7 @@ pub fn create_view(project: &Project, machine: Id) -> Result { projection: ProjectionKind::StateMachine { machine }, nodes: BTreeMap::new(), routes: vec![], + view_options: Default::default(), }, ); let members: Vec<_> = project diff --git a/crates/oa-desktop/src/behavior/state_menu.rs b/crates/oa-desktop/src/behavior/state_menu.rs index d3f4522c..a50a5e8d 100644 --- a/crates/oa-desktop/src/behavior/state_menu.rs +++ b/crates/oa-desktop/src/behavior/state_menu.rs @@ -41,6 +41,7 @@ pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option> { if let Some(context) = context.filter(|context| context.diagram == id) { entries.extend([ item("View properties", Message::EditProperties(id), true), + item("Duplicate view", Message::DuplicateDiagram(id), true), item("Fit diagram", diagram_event(Event::Fit(context)), true), item( "Create another state view", diff --git a/crates/oa-desktop/src/connection_tool.rs b/crates/oa-desktop/src/connection_tool.rs index e7ad2feb..83392c75 100644 --- a/crates/oa-desktop/src/connection_tool.rs +++ b/crates/oa-desktop/src/connection_tool.rs @@ -6,6 +6,7 @@ use std::collections::BTreeSet; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Tool { Interaction, + CommunicationMean, FunctionalExchange, BoundaryExchange, } @@ -13,7 +14,7 @@ pub enum Tool { impl Tool { pub fn for_layer(layer: Layer) -> &'static [Self] { match layer { - Layer::Oa => &[Self::Interaction], + Layer::Oa => &[Self::Interaction, Self::CommunicationMean], Layer::Sa => &[Self::FunctionalExchange, Self::BoundaryExchange], } } @@ -21,6 +22,7 @@ impl Tool { pub fn label(self) -> &'static str { match self { Self::Interaction => "Interaction", + Self::CommunicationMean => "Communication means", Self::FunctionalExchange => "Functional exchange", Self::BoundaryExchange => "Boundary exchange", } @@ -29,6 +31,9 @@ impl Tool { pub fn hint(self) -> &'static str { match self { Self::Interaction => "Click a source activity, then a target activity.", + Self::CommunicationMean => { + "Click an operational entity or actor, then another participant. Communication is bidirectional unless you change its orientation." + } Self::FunctionalExchange => { "Click an output port or source function, then an input port or target function. Function cards use automatic ports." } @@ -40,6 +45,9 @@ impl Tool { pub fn can_start(self, project: &Project, source: Id) -> bool { match self { + Self::CommunicationMean => project.elements.get(&source).is_some_and(|e| { + e.layer() == Layer::Oa && matches!(e.kind(), Kind::Entity | Kind::Actor) + }), Self::Interaction => project .elements .get(&source) @@ -57,6 +65,9 @@ impl Tool { return false; } match self { + Self::CommunicationMean => { + validation::can_connect(project, RelationKind::CommunicationMean, source, target) + } Self::Interaction => { validation::can_connect(project, RelationKind::Interaction, source, target) } @@ -129,6 +140,9 @@ impl Tool { } let name = engineering_name(project, self, source, target); let body = match self { + Self::CommunicationMean => { + RelationBody::new(RelationKind::CommunicationMean, source, target) + } Self::Interaction => RelationBody::new(RelationKind::Interaction, source, target), Self::BoundaryExchange => RelationBody::BoundaryExchange { source, @@ -210,7 +224,9 @@ fn engineering_name(project: &Project, tool: Tool, source: Id, target: Id) -> St project.named(id).to_owned() } }; - let arrow = if tool == Tool::BoundaryExchange && !boundary_oriented(project, source) { + let arrow = if tool == Tool::CommunicationMean + || (tool == Tool::BoundaryExchange && !boundary_oriented(project, source)) + { "—" } else { "→" @@ -273,7 +289,10 @@ mod tests { let actor = element(&mut p, Kind::Actor, Layer::Oa, "Dispatcher"); let function = element(&mut p, Kind::Function, Layer::Sa, "Assess report"); let before = p.clone(); - assert_eq!(Tool::for_layer(Layer::Oa), &[Tool::Interaction]); + assert_eq!( + Tool::for_layer(Layer::Oa), + &[Tool::Interaction, Tool::CommunicationMean] + ); assert_eq!( Tool::Interaction.targets(&p, source), BTreeSet::from([target]) @@ -289,6 +308,27 @@ mod tests { assert_eq!(p, before); } + #[test] + fn operational_communication_uses_participants_without_creating_ports() { + let mut p = Project::empty(); + let a = element(&mut p, Kind::Entity, Layer::Oa, "Field team"); + let b = element(&mut p, Kind::Actor, Layer::Oa, "Coordinator"); + let activity = element(&mut p, Kind::Activity, Layer::Oa, "Report"); + let sa = element(&mut p, Kind::System, Layer::Sa, "Controller"); + let before = p.clone(); + assert_eq!(Tool::CommunicationMean.targets(&p, a), BTreeSet::from([b])); + for invalid in [a, activity, sa, Id::new_v4()] { + assert!(Tool::CommunicationMean.command(&p, a, invalid).is_err()); + } + let command = Tool::CommunicationMean.command(&p, a, b).unwrap(); + let (accepted, relation) = created(&p, &command); + assert!(matches!(relation.body, RelationBody::CommunicationMean { + source, target, oriented: false, ref items, + } if source == a && target == b && items.is_empty())); + assert_eq!(accepted.elements, p.elements); + assert_eq!(p, before); + } + #[test] fn explicit_function_ports_keep_owners_contracts_and_do_not_invent_carried_items() { let mut p = Project::empty(); @@ -532,6 +572,8 @@ mod tests { let mut p = Project::empty(); let oa_a = element(&mut p, Kind::Activity, Layer::Oa, "Detect incident"); let oa_b = element(&mut p, Kind::Activity, Layer::Oa, "Assess incident"); + let oa_team = element(&mut p, Kind::Entity, Layer::Oa, "Response team"); + let oa_actor = element(&mut p, Kind::Actor, Layer::Oa, "Dispatcher"); let sa_a = element(&mut p, Kind::Function, Layer::Sa, "Detect report"); let sa_b = element(&mut p, Kind::Function, Layer::Sa, "Assess report"); let system = element(&mut p, Kind::System, Layer::Sa, "System"); @@ -547,6 +589,7 @@ mod tests { let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap(); for (tool, source, target) in [ (Tool::Interaction, oa_a, oa_b), + (Tool::CommunicationMean, oa_team, oa_actor), (Tool::FunctionalExchange, sa_a, sa_b), (Tool::BoundaryExchange, boundary_a, boundary_b), ] { diff --git a/crates/oa-desktop/src/diagram.rs b/crates/oa-desktop/src/diagram.rs index e938f3ed..3de1a10b 100644 --- a/crates/oa-desktop/src/diagram.rs +++ b/crates/oa-desktop/src/diagram.rs @@ -27,6 +27,9 @@ mod lifecycle_tests; #[cfg(test)] #[path = "diagram/port_hit_tests.rs"] mod port_hit_tests; +#[cfg(test)] +#[path = "diagram/view_options_tests.rs"] +mod view_options_tests; #[path = "diagram/port_labels.rs"] mod port_labels; @@ -35,6 +38,7 @@ pub use port_labels::PortLabels; #[derive(Clone, Copy)] pub struct DiagramCanvas<'a> { pub project: &'a Project, + pub view_index: Option<&'a oa_core::view_options::ViewIndex>, pub diagram: Id, pub selected: Id, pub zoom: f32, @@ -429,7 +433,15 @@ impl DiagramCanvas<'_> { } #[cfg(test)] fn preview_view(&self, state: &State) -> ProjectedView { - oa_core::projection::project_diagram(self.project, &self.preview_diagram(state)) + self.projected(&self.preview_diagram(state)) + } + fn projected(&self, diagram: &Diagram) -> ProjectedView { + match self.view_index { + Some(index) => { + oa_core::projection::project_diagram_with_index(self.project, diagram, index) + } + None => oa_core::projection::project_diagram(self.project, diagram), + } } fn scene(&self, state: &State) -> Arc { let preview = if state.context == Some(self.context) { @@ -460,7 +472,7 @@ impl DiagramCanvas<'_> { } else { self.project.diagrams[&self.diagram].clone() }; - let view = oa_core::projection::project_diagram(self.project, &diagram); + let view = self.projected(&diagram); let routed = Arc::new( if let Some(gesture) = state .route_drag @@ -1837,6 +1849,7 @@ mod tests { }; let canvas = DiagramCanvas { project: &p, + view_index: None, context: Context { document: p.id, diagram, @@ -1909,6 +1922,7 @@ mod tests { .id; let canvas = DiagramCanvas { project: &p, + view_index: None, context: Context { document: p.id, diagram, @@ -2032,6 +2046,7 @@ mod tests { let session = Session::new(&p, &view, context, Tool::FunctionalExchange); let canvas = DiagramCanvas { project: &p, + view_index: None, diagram, context, selected: p.id, @@ -2198,6 +2213,7 @@ mod tests { let (p, diagram, edge) = route_fixture(); let canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected: edge, zoom, @@ -2271,6 +2287,7 @@ mod tests { let (p, diagram, edge) = route_fixture(); let canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected: edge, zoom: 0.5, @@ -2365,6 +2382,7 @@ mod tests { let (p, diagram, edge) = route_fixture(); let mut canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected: edge, zoom: 1., @@ -2490,6 +2508,7 @@ mod tests { assert!(svg.contains("data-navigation=\"target\"")); let mut canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected: edge, zoom: 0.7, @@ -2541,6 +2560,7 @@ mod tests { }); let mut canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected: p.id, zoom: 1., @@ -2612,6 +2632,7 @@ mod tests { }); let canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected: edge, zoom: 1., @@ -2642,6 +2663,7 @@ mod tests { let selected = edge.max(duplicate); // UUID ordering must not override selection. let canvas = DiagramCanvas { project: &p, + view_index: None, diagram, selected, zoom: 1., diff --git a/crates/oa-desktop/src/diagram/lifecycle_tests.rs b/crates/oa-desktop/src/diagram/lifecycle_tests.rs index 409e0f00..3734a7c5 100644 --- a/crates/oa-desktop/src/diagram/lifecycle_tests.rs +++ b/crates/oa-desktop/src/diagram/lifecycle_tests.rs @@ -54,6 +54,7 @@ fn bounds() -> Rectangle { fn canvas(app: &App) -> DiagramCanvas<'_> { DiagramCanvas { project: app.project(), + view_index: None, diagram: app.active_diagram.unwrap(), selected: app.selected, zoom: 1., diff --git a/crates/oa-desktop/src/diagram/pointer_tests.rs b/crates/oa-desktop/src/diagram/pointer_tests.rs index 16dd788c..c674871b 100644 --- a/crates/oa-desktop/src/diagram/pointer_tests.rs +++ b/crates/oa-desktop/src/diagram/pointer_tests.rs @@ -62,6 +62,7 @@ fn screen(p: Position, zoom: f32) -> Point { fn canvas(app: &App, zoom: f32) -> DiagramCanvas<'_> { DiagramCanvas { project: app.project(), + view_index: None, diagram: app.active_diagram.unwrap(), selected: app.selected, zoom, diff --git a/crates/oa-desktop/src/diagram/port_hit_tests.rs b/crates/oa-desktop/src/diagram/port_hit_tests.rs index 64848ea5..a65e02b6 100644 --- a/crates/oa-desktop/src/diagram/port_hit_tests.rs +++ b/crates/oa-desktop/src/diagram/port_hit_tests.rs @@ -3,6 +3,7 @@ use super::*; fn canvas(project: &Project, diagram: Id, zoom: f32) -> DiagramCanvas<'_> { DiagramCanvas { project, + view_index: None, diagram, selected: Id::nil(), zoom, diff --git a/crates/oa-desktop/src/diagram/port_labels.rs b/crates/oa-desktop/src/diagram/port_labels.rs index 5f220936..d0d4982d 100644 --- a/crates/oa-desktop/src/diagram/port_labels.rs +++ b/crates/oa-desktop/src/diagram/port_labels.rs @@ -67,7 +67,10 @@ impl canvas::Program for PortLabels<'_> { .as_ref() .is_none_or(|(context, _)| *context != canvas.context) { - *cached = Some((canvas.context, project_view(canvas.project, canvas.diagram))); + *cached = Some(( + canvas.context, + canvas.projected(&canvas.project.diagrams[&canvas.diagram]), + )); } let view = &cached.as_ref().unwrap().1; let Some(port) = cursor @@ -172,6 +175,7 @@ mod tests { fn program(app: &App) -> PortLabels<'_> { PortLabels(DiagramCanvas { project: app.project(), + view_index: None, diagram: app.active_diagram.unwrap(), selected: app.selected, zoom: 1., diff --git a/crates/oa-desktop/src/diagram/view_options_tests.rs b/crates/oa-desktop/src/diagram/view_options_tests.rs new file mode 100644 index 00000000..458dbd2a --- /dev/null +++ b/crates/oa-desktop/src/diagram/view_options_tests.rs @@ -0,0 +1,89 @@ +use super::*; +use oa_core::view_options::{ExchangeLabels, RelationKinds}; + +#[test] +fn communication_svg_uses_saved_visibility_labels_and_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 item = p + .add_element(Kind::ExchangeItem, Layer::Oa, "Data & status", None) + .unwrap(); + let edge = p.relate(RelationKind::CommunicationMean, "Radio ", a, b); + let RelationBody::CommunicationMean { items, .. } = + &mut p.relations.get_mut(&edge).unwrap().body + else { + unreachable!() + }; + items.push(item); + let diagram = p.ensure_diagram(Layer::Oa); + p.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview; + p.place(diagram, a, Position { x: 50., y: 80. }); + p.place(diagram, b, Position { x: 650., y: 80. }); + validate_candidate(&p).unwrap(); + let initial = p.clone(); + let path_key = format!("") + .next() + .unwrap(); + assert!(!edge_path.contains("marker-end")); + assert!(svg.contains("Radio <link>")); + assert_eq!( + p, initial, + "Export must not author presentation or model changes" + ); + p.diagrams + .get_mut(&diagram) + .unwrap() + .view_options + .exchange_labels = ExchangeLabels::Items; + let items_svg = export_svg(&p, diagram); + assert!(items_svg.contains("Data & status")); + let RelationBody::CommunicationMean { oriented, .. } = + &mut p.relations.get_mut(&edge).unwrap().body + else { + unreachable!() + }; + *oriented = true; + let svg = export_svg(&p, diagram); + assert!( + svg.split(&path_key) + .nth(1) + .unwrap() + .split("") + .next() + .unwrap() + .contains("marker-end") + ); + let placements = p.diagrams[&diagram].nodes.clone(); + p.diagrams + .get_mut(&diagram) + .unwrap() + .view_options + .hidden_edges + .push(edge); + assert!(!export_svg(&p, diagram).contains(&path_key)); + p.diagrams + .get_mut(&diagram) + .unwrap() + .view_options + .hidden_edges + .clear(); + p.diagrams + .get_mut(&diagram) + .unwrap() + .view_options + .relation_kinds = RelationKinds::Only(vec![]); + assert!(!export_svg(&p, diagram).contains(&path_key)); + assert_eq!(p.diagrams[&diagram].nodes, placements); + assert_eq!(language::parse(&language::format(&p)).unwrap(), p); +} diff --git a/crates/oa-desktop/src/document.rs b/crates/oa-desktop/src/document.rs index 9a9cb1bb..37c30df2 100644 --- a/crates/oa-desktop/src/document.rs +++ b/crates/oa-desktop/src/document.rs @@ -97,6 +97,7 @@ pub struct Document { current_bytes: usize, saved: Arc, revision: u64, + view_index: std::cell::OnceCell, limits: ProjectLimits, recovered_unsaved: bool, } @@ -126,6 +127,7 @@ impl Document { future: Vec::new(), saved: Arc::new(bundle), revision: 0, + view_index: Default::default(), limits, recovered_unsaved: false, }) @@ -135,6 +137,12 @@ impl Document { &self.current.decoded.project } + /// Semantic reachability is shared by every diagram at this accepted revision. + pub fn view_index(&self) -> &oa_core::view_options::ViewIndex { + self.view_index + .get_or_init(|| oa_core::view_options::ViewIndex::new(self.project())) + } + pub fn bundle(&self) -> &SourceBundle { &self.current.bundle } @@ -220,6 +228,7 @@ impl Document { // Source-position readers must reject their old revision even though // changing the save location does not create an Undo transaction. self.revision = self.revision.saturating_add(1); + self.view_index.take(); } Ok(()) } @@ -349,6 +358,7 @@ impl Document { bytes: std::mem::replace(&mut self.current_bytes, previous.bytes), }); self.revision = self.revision.saturating_add(1); + self.view_index.take(); true } @@ -361,6 +371,7 @@ impl Document { bytes: std::mem::replace(&mut self.current_bytes, next.bytes), }); self.revision = self.revision.saturating_add(1); + self.view_index.take(); true } @@ -382,6 +393,7 @@ impl Document { retained = retained.saturating_sub(self.past.remove(0).bytes); } self.revision = self.revision.saturating_add(1); + self.view_index.take(); true } } @@ -399,6 +411,37 @@ mod tests { Document::from_project(Project::empty(), ProjectLimits::default()).unwrap() } + #[test] + fn cached_view_index_tracks_accepted_creation_undo_and_redo() { + use oa_core::{Relation, RelationBody, RelationKind, view_options::ViewOptions}; + let mut p = Project::empty(); + let source = p + .add_element(Kind::Entity, Layer::Oa, "Team", None) + .unwrap(); + let target = p + .add_element(Kind::Actor, Layer::Oa, "Dispatcher", None) + .unwrap(); + let mut document = Document::from_project(p, ProjectLimits::default()).unwrap(); + let id = Id::new_v4(); + let options = ViewOptions::default(); + let visible = |d: &Document| d.view_index().prepare(Layer::Oa, &options).includes(id); + assert!(!visible(&document)); + document + .execute(Command::CreateRelation(Relation { + id, + alias: "radio".into(), + name: "Radio".into(), + description: String::new(), + body: RelationBody::new(RelationKind::CommunicationMean, source, target), + })) + .unwrap(); + assert!(visible(&document)); + assert!(document.undo()); + assert!(!visible(&document)); + assert!(document.redo()); + assert!(visible(&document)); + } + fn comment(bundle: &SourceBundle, text: &str) -> SourceBundle { let mut changed = bundle.clone(); changed diff --git a/crates/oa-desktop/src/forms.rs b/crates/oa-desktop/src/forms.rs index 733b618b..6d466147 100644 --- a/crates/oa-desktop/src/forms.rs +++ b/crates/oa-desktop/src/forms.rs @@ -16,6 +16,7 @@ use serde_json::{Value, json}; use std::collections::{BTreeMap, BTreeSet}; mod data_fields; mod port_fields; +mod view_fields; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RecordKind { @@ -37,6 +38,11 @@ pub struct ValueBrowser { pub scope: Option, pub page: usize, } +#[derive(Clone, Default)] +pub struct ReferenceBrowser { + pub query: String, + pub page: usize, +} pub struct Draft { pub kind: RecordKind, @@ -50,6 +56,7 @@ pub struct Draft { constructor_drafts: BTreeMap<(String, String), (Value, BTreeMap)>, port_contract_drafts: BTreeMap, pub value_browsers: BTreeMap, + pub reference_browsers: BTreeMap, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -171,6 +178,7 @@ impl Draft { constructor_drafts: BTreeMap::new(), port_contract_drafts: BTreeMap::new(), value_browsers: BTreeMap::new(), + reference_browsers: BTreeMap::new(), }; draft.rebuild(); Some(draft) @@ -653,7 +661,7 @@ fn friendly(name: &str) -> String { "incoming_items" => "Incoming exchange items".into(), "outgoing_items" => "Outgoing exchange items".into(), "orientation" => "Flow orientation".into(), - "oriented" => "Directed component exchange".into(), + "oriented" => "Directed exchange".into(), "Unset" => "Not specified".into(), "minimum" => "Minimum count".into(), "maximum" => "Maximum count".into(), @@ -986,6 +994,7 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec { _ => matches!( r.kind(), RelationKind::Interaction + | RelationKind::CommunicationMean | RelationKind::FunctionalExchange | RelationKind::BoundaryExchange ), @@ -1656,6 +1665,7 @@ pub fn view<'a>(draft: &'a Draft, project: &'a Project) -> View<'a, Message> { "baseline", "layer", "projection", + "view_options", "element", "diagram", "position", @@ -1701,6 +1711,9 @@ fn property<'a>( label: &str, skip_variant: bool, ) -> View<'a, Message> { + if let Some(view) = view_fields::property(draft, project, value, path) { + return view; + } if let Some(view) = data_fields::property(draft, project, value, path) { return view; } @@ -2563,6 +2576,7 @@ mod tests { layer: Layer::Oa, projection: ProjectionKind::Overview, routes: Vec::new(), + view_options: Default::default(), nodes: BTreeMap::new(), }, ); diff --git a/crates/oa-desktop/src/forms/data_fields.rs b/crates/oa-desktop/src/forms/data_fields.rs index a00e35e4..291739c9 100644 --- a/crates/oa-desktop/src/forms/data_fields.rs +++ b/crates/oa-desktop/src/forms/data_fields.rs @@ -519,11 +519,12 @@ mod tests { }; state } - fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::Event) { + fn semantic_send(app: &mut crate::app::App, event: crate::semantic_review::RowEvent) { + let event = semantic_state(app).address_event(event); let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event))); } fn semantic_decide(app: &mut crate::app::App, facet: FieldFacet, decision: SemanticDecision) { - use crate::semantic_review::Event; + use crate::semantic_review::RowEvent as Event; semantic_send(app, Event::Select(FacetSelector::Field(facet))); semantic_send( app, @@ -548,7 +549,7 @@ mod tests { #[test] fn semantic_reference_choices_follow_selected_mechanism_and_revert_on_keep() { - use crate::semantic_review::{Event, State}; + use crate::semantic_review::{RowEvent as Event, State}; let mut p = Project::empty(); let oa = p .add_element(Kind::ExchangeItem, Layer::Oa, "Source", None) @@ -610,7 +611,7 @@ mod tests { #[test] fn independent_review_combined_take_type_informs_actual_default_form_in_both_directions() { - use crate::semantic_review::Event; + use crate::semantic_review::RowEvent as Event; for reverse in [false, true] { for decision in [SemanticDecision::TakeSource, SemanticDecision::Candidate] { let mut p = Project::empty(); diff --git a/crates/oa-desktop/src/forms/view_fields.rs b/crates/oa-desktop/src/forms/view_fields.rs new file mode 100644 index 00000000..2aea1b16 --- /dev/null +++ b/crates/oa-desktop/src/forms/view_fields.rs @@ -0,0 +1,645 @@ +//! Closed view intent and named category membership through ordinary property drafts. +use super::*; +use iced::widget::column; +use oa_core::view_options::{ + self, CategorySelection, ContextSelection, Coverage, EdgeAvailability, EdgeKind, + ExchangeFamily, ExchangeLabels, OmissionReason, RelationKinds, ViewIndex, ViewOptions, +}; + +const PAGE_SIZE: usize = 24; + +struct Candidate { + id: Id, + label: String, + detail: String, +} + +fn choice<'a>( + label: &str, + path: &str, + selected: &'static str, + choices: Vec<(&'static str, &'static str)>, +) -> View<'a, Message> { + let path = path.to_owned(); + let selected = choices + .iter() + .find(|(_, value)| *value == selected) + .map(|(label, _)| *label); + let labels: Vec<_> = choices.iter().map(|(label, _)| *label).collect(); + column![ + text(label.to_owned()).size(12).color(style::MUTED), + pick_list(labels, selected, move |label| Message::FieldShape( + path.clone(), + json!( + choices + .iter() + .find(|(candidate, _)| *candidate == label) + .unwrap() + .1 + ) + )) + .text_size(12) + .padding(7) + .width(Fill) + ] + .spacing(5) + .width(Fill) + .into() +} + +fn named(project: &Project, id: Id, detail: impl Into) -> Candidate { + let alias = project + .elements + .get(&id) + .map(|element| element.alias.as_str()) + .or_else(|| { + project + .relations + .get(&id) + .map(|relation| relation.alias.as_str()) + }) + .or_else(|| project.traces.get(&id).map(|trace| trace.alias.as_str())) + .unwrap_or_default(); + Candidate { + id, + label: format!("{} · {}", project.named(id), alias), + detail: detail.into(), + } +} + +fn list_value(ids: &[Id], wrapper: Option<&str>) -> Value { + match wrapper { + Some(wrapper) => json!({wrapper: ids}), + None => json!(ids), + } +} + +fn members<'a>( + draft: &'a Draft, + path: &str, + selected: &[Id], + mut candidates: Vec, + wrapper: Option<&'static str>, + verb: &'static str, +) -> View<'a, Message> { + // Preserve and expose now-ineligible identities until the engineer removes them. + for id in selected { + if !candidates.iter().any(|candidate| candidate.id == *id) { + candidates.push(Candidate { + id: *id, + label: format!("Retained reference · {id}"), + detail: + "Missing or outside the chosen layer/family. Remove or correct before Apply." + .into(), + }); + } + } + candidates.sort_by_key(|candidate| (candidate.label.to_lowercase(), candidate.id)); + let browser = draft.reference_browsers.get(path); + let query = browser.map(|browser| browser.query.as_str()).unwrap_or(""); + let search = query.to_lowercase(); + candidates.retain(|candidate| { + format!("{} {} {}", candidate.label, candidate.id, candidate.detail) + .to_lowercase() + .contains(&search) + }); + let page = browser + .map(|browser| browser.page) + .unwrap_or(0) + .min(candidates.len().saturating_sub(1) / PAGE_SIZE); + let start = page * PAGE_SIZE; + let path_owned = path.to_owned(); + let mut out = column![ + text_input("Find by name, alias, identity or reason", query) + .on_input(move |query| Message::FieldReferenceBrowse(path_owned.clone(), query, 0)) + .size(12) + .padding(7), + text(format!( + "{} {} · {} matching", + selected.len(), + if verb == "Hide" { + "explicitly hidden" + } else { + "selected" + }, + candidates.len() + )) + .size(11) + .color(style::MUTED) + ] + .spacing(6) + .width(Fill); + if candidates.is_empty() { + out = out.push(text("No matching records.").size(12)); + } + for candidate in candidates.iter().skip(start).take(PAGE_SIZE) { + let checked = selected.contains(&candidate.id); + let id = candidate.id; + let selected = selected.to_vec(); + let path = path.to_owned(); + out = out.push( + column![ + checkbox(checked) + .label(format!("{verb} {}", candidate.label)) + .size(14) + .text_size(12) + .on_toggle(move |enabled| { + let mut ids = selected.clone(); + ids.retain(|existing| *existing != id); + if enabled { + ids.push(id); + } + Message::FieldShape(path.clone(), list_value(&ids, wrapper)) + }), + text(candidate.detail.clone()).size(11).color(style::MUTED) + ] + .spacing(3) + .width(Fill), + ); + } + if candidates.len() > PAGE_SIZE { + let mut previous = button(text("Previous").size(11)).style(style::nav); + if page > 0 { + previous = previous.on_press(Message::FieldReferenceBrowse( + path.into(), + query.into(), + page - 1, + )); + } + let mut next = button(text("Next").size(11)).style(style::nav); + if start + PAGE_SIZE < candidates.len() { + next = next.on_press(Message::FieldReferenceBrowse( + path.into(), + query.into(), + page + 1, + )); + } + out = out.push( + row![ + previous, + text(format!( + "Page {} of {}", + page + 1, + candidates.len().div_ceil(PAGE_SIZE) + )) + .size(11), + next + ] + .spacing(6), + ); + } + out.into() +} + +fn categories<'a>( + draft: &'a Draft, + project: &Project, + layer: Layer, + path: &str, + family: ExchangeFamily, + selection: &CategorySelection, + cross_layer: bool, +) -> View<'a, Message> { + let label = match family { + ExchangeFamily::Work => "Work exchange categories", + ExchangeFamily::Participant => "Participant exchange categories", + }; + let all = matches!(selection, CategorySelection::All); + let path_owned = path.to_owned(); + let mut out = column![ + text(label).size(13), + checkbox(all) + .label("All, including uncategorized exchanges") + .text_size(12) + .on_toggle(move |all| Message::FieldShape( + path_owned.clone(), + if all { + json!(CategorySelection::All) + } else { + json!(CategorySelection::Any(vec![])) + } + )) + ] + .spacing(6) + .width(Fill); + if let CategorySelection::Any(selected) = selection { + out = out.push( + text("Selected categories combine by union. Selecting none shows none of this family.") + .size(11) + .color(style::MUTED), + ); + let candidates = project + .elements + .values() + .filter_map(|element| { + let SharedBody::ExchangeCategory { + family: candidate_family, + exchanges, + } = element.shared()? + else { + return None; + }; + ((cross_layer || element.layer() == layer) && *candidate_family == family).then( + || { + named( + project, + element.id, + format!("{} exchange members", exchanges.len()), + ) + }, + ) + }) + .collect(); + out = out.push(members( + draft, + path, + selected, + candidates, + Some("Any"), + "Include", + )); + } + out.into() +} + +fn reason(reason: OmissionReason) -> &'static str { + match reason { + OmissionReason::ExplicitHide => "explicitly hidden", + OmissionReason::RelationKind => "relationship kind excluded", + OmissionReason::Category(ExchangeFamily::Work) => "outside selected work categories", + OmissionReason::Category(ExchangeFamily::Participant) => { + "outside selected participant categories" + } + OmissionReason::Coverage => "outside selected global behavior coverage", + OmissionReason::WorkContext => "outside selected process, chain or scenario", + OmissionReason::OutsideProjection => "outside this projection", + OmissionReason::EndpointNotShown { + source: true, + target: true, + } => "source and target are not shown", + OmissionReason::EndpointNotShown { + source: true, + target: false, + } => "source is not shown", + OmissionReason::EndpointNotShown { + source: false, + target: true, + } => "target is not shown", + OmissionReason::EndpointNotShown { + source: false, + target: false, + } => "endpoint placement unavailable", + OmissionReason::FoldedInternal => "internal to a collapsed or grouped node", + OmissionReason::WrongLayer => "outside this layer", + OmissionReason::UnknownEdge => "edge no longer exists", + } +} + +fn options<'a>( + draft: &'a Draft, + project: &Project, + path: &str, + options: ViewOptions, +) -> View<'a, Message> { + let diagram = serde_json::from_value::(draft.context_value()) + .ok() + .or_else(|| project.diagrams.get(&draft.original).cloned()); + let Some(diagram) = diagram else { + return text("View options require a saved view.").size(12).into(); + }; + let layer = diagram.layer; + let cross_layer = diagram.projection == ProjectionKind::Traceability; + let kinds_path = format!("{path}/relation_kinds"); + let all_kinds = matches!(options.relation_kinds, RelationKinds::All); + let toggle_path = kinds_path.clone(); + let mut out = column![text("Saved view visibility").size(15), + text("Apply changes to this view. Model relationships, nesting and saved routes remain available.").size(11).color(style::MUTED), + choice("Exchange labels", &format!("{path}/exchange_labels"), match options.exchange_labels { ExchangeLabels::Name => "Name", ExchangeLabels::Items => "Items", ExchangeLabels::NameAndItems => "NameAndItems" }, vec![("Names", "Name"), ("Items", "Items"), ("Names and items", "NameAndItems")]), + checkbox(all_kinds).label("All relationship line kinds").text_size(12).on_toggle(move |all| Message::FieldShape(toggle_path.clone(), if all { json!(RelationKinds::All) } else { json!(RelationKinds::Only(vec![])) }))].spacing(10).width(Fill); + if let RelationKinds::Only(selected) = &options.relation_kinds { + out = out.push(text("No selected kinds means no relationship lines. Nesting and state action rows remain.").size(11).color(style::MUTED)); + for kind in RelationKind::ALL + .into_iter() + .map(EdgeKind::Relation) + .chain([EdgeKind::Trace]) + { + let label = match kind { + EdgeKind::Relation(kind) => kind.to_string(), + EdgeKind::Trace => "Realization traces".into(), + }; + let checked = selected.contains(&kind); + let selected = selected.clone(); + let path = kinds_path.clone(); + out = out.push(checkbox(checked).label(label).text_size(12).on_toggle( + move |enabled| { + let mut kinds = selected.clone(); + kinds.retain(|existing| *existing != kind); + if enabled { + kinds.push(kind); + } + Message::FieldShape(path.clone(), json!(RelationKinds::Only(kinds))) + }, + )); + } + } + out = out.push(categories(draft, project, layer, &format!("{path}/work_categories"), ExchangeFamily::Work, &options.work_categories, cross_layer)) + .push(categories(draft, project, layer, &format!("{path}/participant_categories"), ExchangeFamily::Participant, &options.participant_categories, cross_layer)) + .push(choice("Work exchange coverage across the whole model", &format!("{path}/coverage"), match options.coverage { Coverage::All => "All", Coverage::InBehavior => "InBehavior", Coverage::OutsideBehavior => "OutsideBehavior" }, vec![("All work exchanges", "All"), ("Used in behavior", "InBehavior"), ("Outside behavior", "OutsideBehavior")])) + .push(text("Coverage uses explicit exchange occurrences in processes, chains and scenarios, including reuse. It does not infer execution or participant transport coverage.").size(11).color(style::MUTED)); + let context_path = format!("{path}/work_contexts"); + let toggle_path = context_path.clone(); + out = out.push( + checkbox(matches!(options.work_contexts, ContextSelection::All)) + .label("All work contexts") + .text_size(12) + .on_toggle(move |all| { + Message::FieldShape( + toggle_path.clone(), + if all { + json!(ContextSelection::All) + } else { + json!(ContextSelection::Any(vec![])) + }, + ) + }), + ); + if let ContextSelection::Any(selected) = &options.work_contexts { + let candidates = project + .elements + .values() + .filter(|element| { + (cross_layer || element.layer() == layer) + && matches!( + element.kind(), + Kind::Process | Kind::FunctionalChain | Kind::Scenario + ) + }) + .map(|element| named(project, element.id, element.kind().label())) + .collect(); + out = out.push(text("Show work exchanges used by any selected context, intersected with the coverage and category choices above. None selected shows no work exchanges.").size(11).color(style::MUTED)) + .push(members(draft, &context_path, selected, candidates, Some("Any"), "Include")); + } + // Both expensive indexes are built once for the panel, never once per edge. + let index = ViewIndex::new(project); + let filter = if cross_layer { + index.prepare_cross_layer(layer, &options) + } else { + index.prepare(layer, &options) + }; + let availability = projection::edge_availability(project, &diagram); + let mut shown = 0usize; + let candidates: Vec<_> = project + .relations + .keys() + .chain(project.traces.keys()) + .filter(|id| { + cross_layer || project.layer_of(**id) == Some(layer) || project.traces.contains_key(id) + }) + .map(|id| { + let reasons = filter.reasons( + *id, + availability + .get(id) + .copied() + .unwrap_or(EdgeAvailability::OutsideProjection), + ); + shown += usize::from(reasons.is_empty()); + named( + project, + *id, + if reasons.is_empty() { + "Shown by these settings".into() + } else { + reasons + .into_iter() + .map(reason) + .collect::>() + .join("; ") + }, + ) + }) + .collect(); + out = out.push(text("Individual lines and omission reasons").size(13)) + .push(text(format!("With these settings: {shown} graphical lines shown · {} lines omitted", candidates.len() - shown)).size(11).color(style::MUTED)) + .push(text("Check Hide to retain an explicit hide. Unchecking removes only that hide; other listed reasons can still omit the line.").size(11).color(style::MUTED)) + .push(button(text("Clear all explicit hides").size(11)).style(style::nav).on_press(Message::FieldShape(format!("{path}/hidden_edges"), json!([])))) + .push(members(draft, &format!("{path}/hidden_edges"), &options.hidden_edges, candidates, None, "Hide")); + out.into() +} + +fn category_members<'a>( + draft: &'a Draft, + project: &Project, + path: &str, + value: &Value, +) -> View<'a, Message> { + let context = draft.context_value(); + let parent = path + .rsplit_once('/') + .map(|(parent, _)| parent) + .unwrap_or_default(); + let family = context + .pointer(&format!("{parent}/family")) + .and_then(|value| serde_json::from_value::(value.clone()).ok()) + .unwrap_or(ExchangeFamily::Work); + let layer = project.layer_of(draft.original).unwrap_or(Layer::Oa); + let Ok(selected) = serde_json::from_value::>(value.clone()) else { + return column![text("The retained member list contains an invalid reference. Clear it explicitly or cancel this property draft before choosing members.").size(12), + button(text("Clear invalid member list").size(11)).style(style::nav).on_press(Message::FieldShape(path.into(), json!([])))].spacing(6).width(Fill).into(); + }; + let candidates = project + .relations + .values() + .filter(|relation| { + view_options::validate_category(project, layer, None, family, &[relation.id]).is_empty() + }) + .map(|relation| { + named( + project, + relation.id, + format!( + "{} → {} · {}", + project.named(relation.source()), + project.named(relation.target()), + relation.kind() + ), + ) + }) + .collect(); + column![text("Exchange members").size(13), text("Membership is a reusable concern. It does not allocate exchanges, change endpoints or imply execution. An empty category is valid.").size(11).color(style::MUTED), + members(draft, path, &selected, candidates, None, "Include")].spacing(6).width(Fill).into() +} + +pub(super) fn property<'a>( + draft: &'a Draft, + project: &'a Project, + value: &'a Value, + path: &str, +) -> Option> { + if path == "/view_options" { + return Some(match serde_json::from_value::(value.clone()) { + Ok(value) => options(draft, project, path, value), + Err(error) => text(format!("View settings are invalid: {error}. The draft remains available for correction in source.")).size(12).into(), + }); + } + if path.ends_with("/ExchangeCategory") { + return Some( + column![ + choice( + "Exchange family", + &format!("{path}/family"), + if value.get("family").and_then(Value::as_str) == Some("Participant") { + "Participant" + } else { + "Work" + }, + vec![ + ("Work exchanges", "Work"), + ("Participant exchanges", "Participant") + ] + ), + category_members( + draft, + project, + &format!("{path}/exchanges"), + value.get("exchanges").unwrap_or(&Value::Null) + ) + ] + .spacing(10) + .width(Fill) + .into(), + ); + } + if path.contains("/ExchangeCategory/") { + if path.ends_with("/family") { + return Some(choice( + "Exchange family", + path, + if value.as_str() == Some("Participant") { + "Participant" + } else { + "Work" + }, + vec![ + ("Work exchanges", "Work"), + ("Participant exchanges", "Participant"), + ], + )); + } + if path.ends_with("/exchanges") { + return Some(category_members(draft, project, path, value)); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn selective_view_property_apply_recovery_and_undo_preserve_semantics_and_empty_choices() { + let mut project = Project::empty(); + let a = project + .add_element(Kind::Activity, Layer::Oa, "Request", None) + .unwrap(); + let b = project + .add_element(Kind::Activity, Layer::Oa, "Provide", None) + .unwrap(); + let exchange = project.relate(RelationKind::Interaction, "Request data", a, b); + let diagram = project.ensure_diagram(Layer::Oa); + project.place(diagram, a, Position { x: 10., y: 20. }); + project.place(diagram, b, Position { x: 300., y: 20. }); + let mut draft = Draft::from_project(&project, diagram).unwrap(); + draft + .shape( + "/view_options/work_categories", + list_value(&[], Some("Any")), + ) + .unwrap(); + draft + .shape("/view_options/hidden_edges", list_value(&[exchange], None)) + .unwrap(); + draft + .shape("/view_options/exchange_labels", json!("NameAndItems")) + .unwrap(); + draft.reference_browsers.insert( + "/view_options/hidden_edges".into(), + ReferenceBrowser { + query: "Request".into(), + page: 3, + }, + ); + let restored = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap(); + assert_eq!( + restored.materialize().unwrap(), + draft.materialize().unwrap() + ); + assert!(restored.reference_browsers.is_empty()); + let mut session = Session::new(project.clone()); + session.execute(restored.command().unwrap()).unwrap(); + let accepted = session.project(); + assert_eq!(accepted.elements, project.elements); + assert_eq!(accepted.relations, project.relations); + assert_eq!( + accepted.diagrams[&diagram].nodes, + project.diagrams[&diagram].nodes + ); + assert_eq!( + accepted.diagrams[&diagram].view_options.work_categories, + CategorySelection::Any(vec![]) + ); + assert_eq!( + accepted.diagrams[&diagram].view_options.hidden_edges, + vec![exchange] + ); + assert_eq!( + language::parse(&language::format(accepted)).unwrap(), + *accepted + ); + session.undo(); + assert_eq!(session.project(), &project); + } + + #[test] + fn category_family_change_retains_members_until_explicit_correction() { + let mut project = Project::empty(); + let a = project + .add_element(Kind::Activity, Layer::Oa, "Request", None) + .unwrap(); + let b = project + .add_element(Kind::Activity, Layer::Oa, "Provide", None) + .unwrap(); + let exchange = project.relate(RelationKind::Interaction, "Request data", a, b); + let id = project + .add_element(Kind::ExchangeCategory, Layer::Oa, "Service concern", None) + .unwrap(); + *project.elements.get_mut(&id).unwrap().shared_mut().unwrap() = + SharedBody::ExchangeCategory { + family: ExchangeFamily::Work, + exchanges: vec![exchange], + }; + let mut draft = Draft::from_project(&project, id).unwrap(); + let path = "/body/Oa/Shared/ExchangeCategory"; + draft + .shape(&format!("{path}/family"), json!("Participant")) + .unwrap(); + assert_eq!( + draft + .materialize() + .unwrap() + .pointer(&format!("{path}/exchanges")), + Some(&json!([exchange])) + ); + assert!(execute_batch(&project, &[draft.command().unwrap()]).is_err()); + let mut restored = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap(); + restored + .shape(&format!("{path}/exchanges"), list_value(&[], None)) + .unwrap(); + let accepted = execute_batch(&project, &[restored.command().unwrap()]).unwrap(); + assert!( + matches!(accepted.elements[&id].shared(), Some(SharedBody::ExchangeCategory { family: ExchangeFamily::Participant, exchanges }) if exchanges.is_empty()) + ); + assert_eq!(accepted.relations, project.relations); + } +} diff --git a/crates/oa-desktop/src/guide.rs b/crates/oa-desktop/src/guide.rs index 0accf6a2..049129b7 100644 --- a/crates/oa-desktop/src/guide.rs +++ b/crates/oa-desktop/src/guide.rs @@ -25,8 +25,10 @@ pub enum Topic { States, Interfaces, Transitions, + SemanticReview, Artifacts, Source, + ModelChecks, Recovery, Files, Server, @@ -44,8 +46,12 @@ pub enum Event { Topic(Topic), } +pub(crate) const ARTICLE_SCROLL_ID: &str = "engineering-guide-article"; + impl State { - pub fn update(&mut self, event: Event) { + /// Returns whether navigation selected a different article. + pub fn update(&mut self, event: Event) -> bool { + let previous = self.topic; match event { Event::Search(query) => { self.query = query; @@ -62,11 +68,12 @@ impl State { self.query.clear(); } } + self.topic != previous } } impl Topic { - const ALL: [Self; 16] = [ + const ALL: [Self; 18] = [ Self::FirstProject, Self::Perspectives, Self::Participants, @@ -78,8 +85,10 @@ impl Topic { Self::States, Self::Interfaces, Self::Transitions, + Self::SemanticReview, Self::Artifacts, Self::Source, + Self::ModelChecks, Self::Recovery, Self::Files, Self::Server, @@ -98,9 +107,11 @@ impl Topic { Self::States => "States, modes & transitions", Self::Interfaces => "Data types, ports & interfaces", Self::Transitions => "Move between OA & SA", + Self::SemanticReview => "Review semantic changes", Self::Artifacts => "Connect engineering evidence", Self::Source => "Textual editing & Git", - Self::Recovery => "Checks & error recovery", + Self::ModelChecks => "Read model checks", + Self::Recovery => "Drafts & crash recovery", Self::Files => "Files & keyboard shortcuts", Self::Server => "Hosted models & REST API", } @@ -113,8 +124,10 @@ impl Topic { "MODEL THE SYSTEM" } Self::Chains | Self::Scenarios | Self::States | Self::Interfaces => "DESCRIBE BEHAVIOR", - Self::Transitions | Self::Artifacts | Self::Source => "CONNECT YOUR WORK", - Self::Recovery | Self::Files | Self::Server => "REFERENCE", + Self::Transitions | Self::SemanticReview | Self::Artifacts | Self::Source => { + "CONNECT YOUR WORK" + } + Self::ModelChecks | Self::Recovery | Self::Files | Self::Server => "REFERENCE", } } } @@ -177,13 +190,23 @@ fn article(topic: Topic) -> Article { "Choose Entity under Create element, then + At root. Name it Rescue coordinator and Apply changes. Use Actor for an external participant such as a Vessel in distress.", "Select Rescue coordinator, choose Activity, then + Allocated. Name the new activity Assess incident. It appears inside its performer; an Allocation relationship records that responsibility.", "Add a second activity for another participant. Select an activity, choose + Relationship, select Interaction and choose the compatible target. Name the exchange after what is conveyed.", - "Visit Model checks to review unfinished work. Save project writes the accepted model and applied drafts to your chosen file.", + "Visit Model checks to review unfinished work. Save all writes the accepted model and applied drafts to your chosen file.", ], ), explain( "Continue into system design", "Use Create / update counterpart on an operational element to review an OA → SA mapping. In System Analysis, identify the System under study, its external Actors and the Functions for which each is responsible. The transition tutorial explains scope, reuse and review decisions.", ), + walkthrough( + "Build a reviewable OA → SA baseline", + &[ + "State the mission and capability: for example, protect life at sea by coordinating a rescue. Link the mission to the capability and identify the participants that support it.", + "Describe the normal operational Process and an exception Scenario using the activities and Interactions already in the architecture. Give the scenario an explicit Context capability and connect the process with Chain involvement.", + "Review which operational work the System will support. Create or reuse its SA counterparts, then check the resulting System boundary, external actors, leaf Function allocations and exchanged payloads.", + "Refine the system Functional chain, Scenario and State machine. Specify data contracts and entry/do/exit work where they clarify responsibilities. Link requirements, analyses or tests to the particular records they support.", + "Read Model checks, review changes through counterpart traces, and inspect both perspectives. Save the complete source project and review its Git diff as one engineering milestone.", + ], + ), explain( "Explore a worked model", "Open the coastal rescue example to inspect allocation, decomposition, repeated scenario messages, operating modes, typed interfaces and artifact references. Loading the example uses the normal unsaved-work prompt. It replaces the current project only after that workflow completes.", @@ -194,6 +217,7 @@ fn article(topic: Topic) -> Article { Topic::Participants, Topic::Nesting, Topic::Transitions, + Topic::ModelChecks, ], destination: Some((Page::Diagram, "Open Architecture")), } @@ -264,7 +288,7 @@ fn article(topic: Topic) -> Article { const { Article { summary: "A diagram is a view of the model. Its boxes, positions and nesting reveal selected relationships; moving a box does not redefine engineering ownership.", - keywords: "containment semantic parent hierarchy nested nesting allocation deployment diagram placement arrange fit fold hide zoom resize routing connection bend waypoint detour label reset", + keywords: "containment semantic parent hierarchy nested nesting allocation deployment diagram placement arrange fit fold hide zoom resize routing connection bend waypoint detour label reset selective visibility category categories coverage process context duplicate copy items", sections: &[ explain( "Three things to distinguish", @@ -281,7 +305,7 @@ fn article(topic: Topic) -> Article { walkthrough( "Choose and edit the relationship you mean", &[ - "Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective and projection.", + "Use the diagram selector or + View to work with a focused diagram. View properties edits its name, perspective, projection and Saved view visibility. Apply changes commits the view settings.", "Choose Containment for semantic children, Architecture · allocated work for responsibilities, Functional dataflow for work and exchanges, or Missions & capabilities for outcomes. All relationships is a broader exploration view; OA ↔ SA traceability displays counterparts.", "Select an element and edit Semantic parent to change ownership. Use an Allocation relationship to change responsibility. Apply changes validates the semantic change.", "Drag nodes to arrange the current view, scroll to zoom and use Fit to frame its contents. Arrange changes diagram geometry. Export SVG produces a shareable view.", @@ -292,6 +316,16 @@ fn article(topic: Topic) -> Article { "Keep diagrams readable", "Use separate views for responsibility, decomposition and cross-layer coverage. Place existing elements with Show in this diagram. A placement's Diagram field can relocate it while preserving its identity and artifact references. Do not duplicate a semantic element merely to show it in another view.", ), + walkthrough( + "Save a focused exchange view", + &[ + "To preserve the current presentation, right-click empty diagram space and choose Duplicate view. The copy has independent placements, routes and visibility settings while representing the same model elements and relationships. View-specific artifact links stay with the original view.", + "Open View properties. Under Work exchange categories or Participant exchange categories, uncheck All, including uncategorized exchanges, then Include the categories you need. Selected categories combine by union; none selected shows none of that family. Work means OA Interactions or SA Functional exchanges; participant means OA Communication means or SA Boundary exchanges, including supported legacy records.", + "Work exchange coverage across the whole model offers All work exchanges, Used in behavior and Outside behavior. This uses explicit process, chain and scenario occurrences, including reuse. To focus on particular behavior, uncheck All work contexts and Include those processes, chains or scenarios. Context and coverage intersect with categories; no selected contexts means no work lines.", + "Choose Exchange labels: Names, Items or Names and items. In Individual lines and omission reasons, search a name or alias and check Hide to retain an explicit hide. Unchecking Hide removes only that reason; other settings or missing endpoints may still omit the line. Clear all explicit hides removes the manual hide list.", + "Apply changes and inspect the result. To restore all eligible lines, enable All relationship line kinds, both category All choices and All work contexts, choose All work exchanges, and clear explicit hides. Empty relationship-kind selection shows no lines. These settings do not remove nodes or model facts; a line still needs endpoints eligible for the projection.", + ], + ), explain( "Read allocation summaries and ports", "A dashed parent activity/function with an italic name and SUMMARY heading groups leaves assigned to the same performer. It has no direct allocation of its own; its inspector explains the leaf performer. Small ports belong to the boundary of their visible owner and are selectable model elements. Folding a container hides its inner work and ports; summary exchange lines do not invent new ports on the container.", @@ -341,7 +375,7 @@ fn article(topic: Topic) -> Article { ), explain( "Use scenarios as concrete evidence", - "A scenario illustrates one path through the behavior: normal response, degraded operation or an exception. Keep its intended capability and assumptions clear in its engineering description and available model relationships. Scenarios complement the capability definition; they do not replace the outcome.", + "A scenario illustrates normal response, degraded operation or an exception. In its Properties, choose Context to name the capability it supports in the same perspective; record assumptions and pre/postconditions too. Connect a Process or Functional chain with Chain involvement. Descriptive prose explains the intent, while these explicit references make the context inspectable.", ), explain( "Avoid implementation names at this stage", @@ -362,7 +396,7 @@ fn article(topic: Topic) -> Article { const { Article { summary: "An exchange says what passes between work or across a participant boundary. Its payload, endpoints and allocation make that statement precise.", - keywords: "interaction functional exchange boundary exchange flow information material energy event source target port exchange allocation", + keywords: "interaction functional exchange boundary exchange communication mean participant directed bidirectional category categories flow information material energy event source target port exchange allocation", sections: &[ walkthrough( "Create a typed exchange", @@ -371,7 +405,7 @@ fn article(topic: Topic) -> Article { "Choose Interaction or Functional exchange in the diagram palette. Click the source, then a highlighted compatible target, or drag between them. Functional exchanges connect output ports to input ports; function cards create or reuse real ports in the same transaction. Middle-button drag pans while keeping the source; arrow keys also pan when the drawing canvas has focus. Click empty space to restart, or Escape to cancel.", "Create an Exchange item under Data & interfaces. Choose its nature (Information, Material or Energy) and its mechanism (Flow, Event, Operation or Shared data). Describe the payload independently of the connection.", "Select the exchange in Model explorer or the diagram. Edit its properties to reference the appropriate exchange items and available ports; Apply changes validates the complete record.", - "Choose Boundary exchange and connect compatible component ports. Flow goes from Out to In; Standard is unoriented. Select / move returns to arranging. Exchange allocation associates a boundary exchange with the functional exchange it carries.", + "In System Analysis, choose Boundary exchange and connect compatible component ports. Flow goes from Out to In; Standard is unoriented. Select / move returns to arranging. Exchange allocation associates a boundary exchange with the functional exchange it carries.", ], ), explain( @@ -382,6 +416,14 @@ fn article(topic: Topic) -> Article { "Use engineering payloads", "Choose Information for data such as a distress report, Energy for delivered power and Material for a transferred substance. Mechanism separately describes how it is exchanged: an Event is a notification, an Operation has parameters, and a Flow carries values or a physical quantity. Data types and fields make payloads precise. Artifact links can point to interface-control documents.", ), + explain( + "Operational and system boundaries", + "An OA Interaction connects activities. A Communication mean connects operational Entities or Actors directly; it requires no system ports. In Operational Analysis, choose Communication means and connect its participants. Its properties hold carried items and direction; the relationship form offers Directed source → target (unchecked: bidirectional). Existing OA Boundary exchanges with Legacy ports remain editable for compatibility.", + ), + explain( + "Relate operational transport and concerns", + "Use Exchange allocation to associate a Communication mean with the Interactions it carries. Allocate each activity to its actual performer and check that those performers match the communication endpoints and direction. Create an Exchange category at the perspective root, choose Work exchanges or Participant exchanges in its properties, and Include existing exchange members. Categories name reusable concerns for view filtering; they do not change endpoints, allocate work or imply execution. A category may be empty, and an exchange may belong to several categories.", + ), ], related: &[ Topic::Interfaces, @@ -404,8 +446,9 @@ fn article(topic: Topic) -> Article { &[ "First create the activities/functions and their exchanges. Check the source and target work for every exchange.", "Open Behavior & scenarios. Create Process in OA or Functional chain in SA using Create element and + At root.", - "Select that behavior in the page's selector. Use + Work or + Exchange to involve an existing source. Each contribution keeps its own identity while referring to the shared engineering definition.", - "Select a contribution, then add another after it or use Connect to wire existing contributions. Branch / parallel creates a split, two paths and their merge in one undoable edit. Enter explicit alternative conditions; use Fit and zoom to inspect the topology.", + "Select that behavior in the page's selector. Choose Work or Exchange, then a highlighted path or contribution position to involve an existing source. Each contribution keeps its own identity while referring to the shared engineering definition.", + "Choose Sequence link and click or drag from the preceding contribution to the following contribution. In Select / reorder, drag a selected path endpoint to reconnect it; a contribution's ↕ handle changes its position within a column. These are local contributions and sequence links, not new architecture exchanges.", + "Select a contribution, then add another after it. Two paths… creates a split, two paths and their merge in one undoable edit. Enter explicit alternative conditions; use Fit and zoom to inspect the topology.", "Use Chain involvement from the Process/Functional chain to the capability it supports. Read Model checks for unfinished or disconnected paths.", ], ), @@ -415,7 +458,7 @@ fn article(topic: Topic) -> Article { ), explain( "Review the path as an engineer", - "Check that exchanges express meaningful handoffs and that the links and branch guards support the selected outcome. Use Reuse to involve an existing process or chain as a nested reference. Imported ordered exchange paths retain their meaning; the first topology edit explains and preserves their implied links in the same transaction.", + "Check that exchanges express meaningful handoffs and that the links and branch guards support the selected outcome. Use Reuse to involve an existing process or chain as a nested reference. For an imported ordered exchange path, Append exchange… continues that path; Convert ordered path to explicit links exposes its implied links for topology editing. Conversion preserves the original order in one undoable change.", ), ], related: &[Topic::Exchanges, Topic::Capabilities, Topic::Scenarios], @@ -436,7 +479,8 @@ fn article(topic: Topic) -> Article { "Use + Lifeline and choose the participant or work it represents. Choices follow the scenario's viewpoint: operational activities, operational participants, system functions or system boundary participants.", "Choose + Message and its style. Call + correlated reply creates both occurrences in one edit. Select the exchange and compatible sending/receiving lifelines. Reply to existing call offers valid unanswered calls.", "Use Action, Observe state or Fragment for other behavior. A local timer has an expression and a lifeline, with no global exchange. Create and delete messages describe participant lifetime; Model checks also flag risks inside repeated loops.", - "Use the ordered list's up/down controls to change sequence order. Click a message or list entry to inspect that occurrence rather than the reusable exchange definition.", + "Choose a message style and Draw message. Start at a highlighted sequence position on a lifeline, then click or drag to the receiving lifeline. The form retains the chosen position while you select the exchange and details. Escape cancels the gesture.", + "In Select / reorder, click an occurrence and drag its ↕ handle to another position within the same sequence. The ordered list's up/down controls provide another way to reorder. Select an occurrence to inspect its particular use of the exchange, including its message values.", ], ), walkthrough( @@ -452,6 +496,10 @@ fn article(topic: Topic) -> Article { "Conditions are engineering statements", "Guards, actions and effects document behavior; a drawn scenario does not execute code or prove timing. Add evidence references when a simulation, test or analysis establishes a stronger claim.", ), + explain( + "Keep uses distinct from definitions", + "Several occurrences can refer to one exchange without duplicating it. Edit occurrence properties for this message's sender, receiver and value expressions; edit the exchange for the shared endpoints and payload. A Reply refers to a particular Call, not merely a similar name. Reordering across fragment operands or changing lifeline bindings needs an explicit structural edit, not an accidental drag across a boundary.", + ), ], related: &[ Topic::Chains, @@ -489,7 +537,16 @@ fn article(topic: Topic) -> Article { ), explain( "Availability and state actions", - "Availability links a participant or behavior to a State/Mode in which it is available. State action links a State/Mode to an Activity/Function and records its entry, do or exit phase. Use these typed relationships when work depends on operating conditions.", + "Availability links a participant or behavior to a State/Mode in which it is available. State action links a State/Mode to existing Activity/Function work and records Entry, Do or Exit. The saved state view shows those named action rows above internal transitions and nested regions. Select a row to edit its actual State action relationship. The linked work remains in its own semantic owner; the row does not contain or copy it. Free-text entry/do/exit notes remain separate engineering prose.", + ), + walkthrough( + "Add timing without losing an existing transition", + &[ + "Select the transition and choose Edit transition. Its current endpoints, kind, existing triggers, guard and effects populate the form.", + "Choose Add a time event with this transition, select After for a delay or At for a time, then enter a time expression such as 30 s. This adds a typed Event to the trigger alternatives; it does not replace existing triggers.", + "Review the complete transition before Apply. The event and transition update are one undoable change. If the form is rejected, correct the retained values; a rejected candidate creates no orphan Event.", + "Save the entry and any included source files. On reopening, check the transition's trigger list and the Event expression. An expression documents timing intent; it is not evaluated by a simulator.", + ], ), explain( "Keep conditions testable", @@ -579,7 +636,7 @@ fn article(topic: Topic) -> Article { ), explain( "Know what reconciliation changes", - "Semantic review supports explicit structural and scalar changes, typed constructors and individual collection rows, including members, literals, annotations and bindings. Candidate validation checks the complete proposed model before applying one undoable transaction. Ordered collections, attachment/artifact and multi-source review controls remain unfinished; some structured facts are read-only. Do not assume Refresh preview automatically synchronizes arbitrary structure or every field.", + "Semantic review supports structural and scalar changes, typed constructors, individual collection rows and explicit ordered collections. Several trace rows can contribute to one combined destination proposal. Candidate validation checks the complete proposed model before one undoable transaction. The Review semantic changes topic explains included rows, mapped references and order decisions. Attached-record and artifact reconciliation remain outside this dialog's authoring controls; use their supported editing workflows.", ), explain( "Avoid duplicate counterparts", @@ -591,6 +648,68 @@ fn article(topic: Topic) -> Article { Topic::Participants, Topic::Capabilities, Topic::Recovery, + Topic::SemanticReview, + ], + destination: Some((Page::Traces, "Open Traceability")), + } + } + } + Topic::SemanticReview => { + const { + Article { + summary: "Review what changed between corresponding OA and SA records. Choose the engineering result explicitly and retain the reasoned correspondence for later changes.", + keywords: "semantic drift review keep take final candidate previously reviewed history reset reference pairs annotation qualified field path binding lifeline membership order up down reconcile steps fragment branch direction multi-source batch", + sections: &[ + walkthrough( + "Compare the right records and direction", + &[ + "From an existing counterpart trace, choose Review semantics…. Confirm the OA → SA or SA → OA direction and select the intended trace / review history. Each correspondence and direction has its own retained row draft; opening another row does not discard the first row's choices.", + "In Properties, select a fact. Compare Previously reviewed with Current on both sides. Unreviewed means there is no prior observation; Both changed means both sides changed since that observation, not that one side is automatically wrong.", + "Choose Keep for the destination, Take for the source, or Set final value…. For a kind change, Set final kind and payload… reviews the complete replacement and its affected references. Leave unselected makes no decision for that fact.", + "Read Reference pairs and Additional changes before Apply reviewed changes. A missing or ambiguous correspondence must be resolved explicitly. Apply commits the validated model and the reviewed history together; Undo restores both.", + ], + ), + explain( + "Map references by meaning", + "Reference pairs connect exact OA and SA identities used by the proposed change. Similar names are not sufficient. Map the records that play the same engineering role, including enclosing fields in qualified message-value paths. Mapping a referenced record does not authorize creating, moving or editing that record. If it needs a counterpart, establish that correspondence first.", + ), + walkthrough( + "Review one message value or lifeline binding", + &[ + "Open Members / rows and choose the exact identities on both sides. For a message value, browse the enclosing record fields as well as the final field: origin.latitude and destination.latitude are distinct addresses.", + "For a binding, choose the referenced lifeline key and its compatible local lifeline. Check which scenario owns each side. Keep or Take concerns that row, not all bindings in the scenario.", + "Use Set final value… to enter the destination expression or local lifeline. Include in destination collection controls that row's presence. Unchecking removes the entry without deleting its referenced engineering object.", + "If a row key changed, review the old entry's removal and the new entry's addition separately. A missing source identity is not the same decision as an identified source entry that has been deleted. Read the proposed value before applying.", + ], + ), + walkthrough( + "Review membership and sequence order separately", + &[ + "Review which occurrences belong to Steps, or which operands belong to Fragment branches, using Members / rows. This changes membership; it does not silently reparent or create the referenced records.", + "Choose Review final order… or the corresponding order fact in Properties. Keep retains destination positions. Take uses mapped source order when it can preserve destination-only members without an ambiguous placement.", + "For an explicit result, choose Set final value…, then use Up and Down. If you change membership afterward, choose Reconcile with selected membership: surviving positions are retained, removed members leave the list, and new members append for you to position.", + "Every surviving destination member must appear exactly once. Resolve an ambiguous insertion with an explicit final order; do not remove an engineering contribution merely to make the preview pass.", + ], + ), + explain( + "Preserve intentional differences and useful history", + "Keep can acknowledge a deliberate difference without overwriting the destination. Reset this exact previous observation clears only the chosen review history; it does not restore an old model value. Overlapping row addresses may require an explicit history reset. Review those messages rather than clearing unrelated observations. Pending review choices and raw values can be recovered after an unexpected exit, but the plan is recomputed against the recovered accepted model.", + ), + walkthrough( + "Review several sources together", + &[ + "Use Choose another trace to add or reopen, then Open row. Include this row determines whether its decisions contribute to the shared preview. The trace / review history selector switches the row you are editing; excluded rows retain their drafts.", + "Review each included source's contribution to the destination. Compatible decisions combine; contradictory Keep, Take or final-value decisions must be resolved explicitly. Read the combined conflicts, Reference pairs and Additional changes before Apply reviewed changes. One Apply commits the combined model and review histories; one Undo restores them.", + "Only one trace history per source can be included. Some reverse correspondences cannot share the core reference mapping; the preview explains the conflict. Exclude a row or choose a distinct correspondence without discarding its draft. Switching direction retains the other direction's choices for later review.", + "Recovery retains included and excluded row drafts and raw final values, then recomputes the plan against the recovered accepted model. Refresh preview is not automatic synchronization and does not authorize changes to attached records or artifacts.", + ], + ), + ], + related: &[ + Topic::Transitions, + Topic::Interfaces, + Topic::Scenarios, + Topic::Recovery, ], destination: Some((Page::Traces, "Open Traceability")), } @@ -608,7 +727,7 @@ fn article(topic: Topic) -> Article { "Select the model record the evidence concerns. This may be an element, relationship, diagram, placement or other supported persistent record.", "Choose + Artifact reference in Properties, or + Artifact in Traceability. Name the reference so another engineer understands its purpose.", "Enter the Artifact URI, optional external Revision and Fragment. Use a commit or document revision when a reproducible reference matters; the fragment identifies a particular part of the artifact.", - "Add a description explaining whether the artifact specifies, implements or verifies the engineering decision. Apply changes, then inspect the owner and reference in Traceability.", + "Choose the relation to the artifact: References, Satisfies, Verifies or Implements. Add a description explaining the evidence and its limits. Apply changes, then inspect the owner and reference in Traceability.", ], ), explain( @@ -659,6 +778,11 @@ fn article(topic: Topic) -> Article { "Use one contract", "The normative specification is docs/language.rst in the distribution. It defines source grammar and supported graphical semantics together. Text preserves semantic records, diagram placements, ordered occurrences, traces and artifact references. Graphical edits preserve unaffected source formatting and comments; accepted model Undo/Redo also restores the exact source files. Comments and source-file organization are not part of JSON export.", ), + example( + "Keep included files with their entry", + "An include is relative to the file that declares it. Put declarations in the included file and keep one project entry. A newly created graphical record may live in another source file; use Open source to locate it and include every affected file in your review and save.", + "// Inside the project declaration:\ninclude \"oa/operations.arcadia\";\ninclude \"sa/model.arcadia\";", + ), explain( "Resolve source-control conflicts before applying", "Ordinary Git conflict markers are not Arcadia syntax. Resolve the text merge, preserve intended UUIDs/references, then apply and inspect Model checks. The editor does not decide which engineer's model change is correct.", @@ -674,6 +798,53 @@ fn article(topic: Topic) -> Article { } } } + Topic::ModelChecks => { + const { + Article { + summary: "Use checks to find invalid structure and unfinished engineering work. A valid model can still need decisions, behavior evidence or allocation.", + keywords: "validation warning error completeness coverage non_leaf_work_exchange work_without_behavior_context exchange_without_behavior_context mission_without_capability capability_without_behavior_context unsupported_capability sequence_gap allocation leaf context diagnostics lint", + sections: &[ + explain( + "Errors and warnings answer different questions", + "An error rejects the candidate, leaving the accepted model unchanged. A warning describes an incomplete or questionable model that can still be saved. Read the record, field and source location rather than treating the number of warnings as a quality score. Fix structural errors first: the completeness pass waits for valid structure, so its warnings may appear after those errors are corrected.", + ), + walkthrough( + "Connect work to explicit behavior", + &[ + "For an exchange on non-leaf work, identify the leaf Activity or Function responsible for that endpoint. In SA, keep its typed input/output port owned by that Function. Do not add a fake child just to suppress the warning.", + "For leaf work without behavior context, include a work action or one of its exchanges in an ordered Process/Functional chain or Scenario. Allocation alone states who performs the work, not how it is used.", + "For an exchange without behavior context, include that exact Interaction or Functional exchange in an exchange occurrence. A similarly named exchange or an unlisted occurrence under the container does not provide evidence.", + "Only included steps, fragment operands and valid reused behavior contribute. State actions, availability links and prose remain useful but do not stand in for an explicit process/scenario use. Coverage demonstrates inclusion, not execution or completeness of every possible path.", + ], + ), + walkthrough( + "Connect purpose to behavioral evidence", + &[ + "For a mission without a capability, connect the mission to the capability that delivers it. Merely involving a participant in the mission does not identify that capability.", + "For a supported capability without behavior context, connect a Process/Functional chain using Chain involvement or set a Scenario's Context to that capability in the same perspective.", + "A capability used only to group child capabilities is treated differently from an isolated leaf. A directly contextualized Scenario establishes support; adding an unrelated participant link is not required merely to clear a warning.", + "Review the engineering statement after each correction. Do not invent relationships solely to achieve a warning-free list; leave an unfinished decision visible until the model can express it accurately.", + ], + ), + explain( + "Review sequence gaps and incomplete contracts", + "A sequence gap calls attention to disconnected handoffs; inspect the contribution endpoints and intended path. Missing allocations, unspecified exchange mechanisms and incomplete conditions need their own engineering decisions. An accepted model does not prove that guards are executable, units are converted, external artifacts exist or timing requirements are met.", + ), + explain( + "The same checks are available to text and automation", + "Model checks, source lint and server validation use the shared model rules. Source-project diagnostics identify the included file and location; native JSON checks identify the model record and field. A validation request can inspect a candidate without committing it. Correct errors in the complete candidate before applying it graphically, as text or through the API.", + ), + ], + related: &[ + Topic::Capabilities, + Topic::Chains, + Topic::Scenarios, + Topic::Recovery, + ], + destination: Some((Page::Validation, "Open Model checks")), + } + } + } Topic::Recovery => { const { Article { @@ -721,7 +892,12 @@ fn article(topic: Topic) -> Article { "New, Open, opening a recent model, loading the example, Recover and Close use the unsaved-work prompt. Save and continue, discard or cancel is your decision. Checkpoints are private recovery copies; continue saving meaningful milestones and using version control or backups.", ), ], - related: &[Topic::Source, Topic::Files, Topic::Transitions], + related: &[ + Topic::ModelChecks, + Topic::Source, + Topic::Files, + Topic::SemanticReview, + ], destination: Some((Page::Validation, "Open Model checks")), } } @@ -734,7 +910,7 @@ fn article(topic: Topic) -> Article { sections: &[ explain( "Start from Home", - "Home offers New model, Open model, the rescue example and this guide. Recent models lists local files you successfully opened or saved. A disconnected drive or moved file remains visible; use Locate to choose its new location or Refresh after reconnecting the drive. Remove forgets the recent entry without deleting its model file. Returning to Home retains the current document and uses the same draft navigation guards as other pages.", + "Home offers New model, Open model, the rescue example and this guide. Recent models lists local files you successfully opened or saved. A disconnected drive or moved file remains visible; use Locate to choose its new location or Refresh after reconnecting the drive. Remove forgets the recent entry without deleting its model file. Home retains the current document; Continue editing returns to the last workspace, including Textual model or a saved state view, with its drafts intact.", ), explain( "Editable formats", @@ -743,7 +919,7 @@ fn article(topic: Topic) -> Article { example( "Keyboard reference", "Shortcuts act on the current project. In Textual model, undo and redo affect the source draft.", - "Ctrl+N New project\nCtrl+O Open model\nCtrl+S Save project\nCtrl+Shift+S Save as\nCtrl+Z Undo\nCtrl+Shift+Z Redo\nCtrl+Y Redo\nEscape Cancel the active dialog", + "Ctrl+N New project\nCtrl+O Open model\nCtrl+S Save all\nCtrl+Shift+S Save as\nCtrl+Z Undo\nCtrl+Shift+Z Redo\nCtrl+Y Redo\nEscape Cancel the active dialog", ), walkthrough( "Save and share a model", @@ -754,6 +930,10 @@ fn article(topic: Topic) -> Article { "If another tool changed or deleted the open file, preserve your candidate using a new Save as destination. Compare and reconcile the versions before replacing either one.", ], ), + explain( + "Save as preserves the engineering history", + "Saving a source project under a new entry name relocates the save destination without adding a model edit or clearing Redo. Choose a name that does not collide with an included file, including one retained in Undo/Redo history. Save all includes every accepted source file; an unchanged entry file alone does not prove that an included file was saved. If a save fails, the current model and drafts remain available.", + ), explain( "Open and migration", "Open validates a complete candidate before replacing the project. Supported earlier versions migrate to the current schema; Model checks shows migration details such as preserved ports, supplied data defaults, generated interface allocations and unresolved message paths. Old Event items become Information notifications; unknown physical mechanisms remain Unspecified. Source upgrades retain files, includes, nested ownership and comments. Opening does not overwrite the original: use Save as if it must remain in its older format. Invalid input leaves the accepted project unchanged.", @@ -806,11 +986,15 @@ fn article(topic: Topic) -> Article { &[ "Create or import a project using /api/v1/projects or /api/v1/projects/import. Keep the returned service resource_id; it is distinct from the editable model UUID.", "Read the project's envelope, source or model. Preserve its exact ETag as the precondition for your edit.", - "Submit typed commands to /transactions, or a complete candidate to /source or /model, using If-Match. Use transaction dry-run or validation before committing substantial generated changes.", + "Submit typed commands to /transactions, or a complete candidate to /source, /model or /source-project, using If-Match. Use transaction dry-run or validation before committing substantial generated changes.", "A stale write returns 412. Keep the user's draft, reload the current head and reconcile deliberately. After an uncertain write outcome, read the server state before retrying.", "Query /artifact-links and /traces to connect domain-tool artifacts to model records. The server stores these references; the integration is responsible for resolving its external artifacts.", ], ), + explain( + "Validate and save a source bundle", + "GET /source-project returns the entry and included files. POST /validation with format source-project and source_project containing that bundle checks a candidate without committing it; warnings can coexist with valid: true. PUT /source-project accepts the complete bundle under the current If-Match ETag. Structural errors reject the write, preserving the accepted model and revision. Compare record IDs and fields across text/JSON diagnostics; source-project results also identify the included file and source span.", + ), explain( "Deployment and persistence", "Non-loopback binding requires a configured bearer token. Use the documented local-disk storage and deployment configuration. The service keeps the current revision and serializes accepted updates; it does not provide retained revision history or server-side undo. Stop the service cleanly before copying its committed store for backup.", @@ -922,6 +1106,13 @@ fn article_view(topic: Topic) -> Element<'static, Message> { ); } if topic == Topic::FirstProject { + content = content.push(text("For a worked model, open the water-pressure walkthrough. Copy the whole examples/release1 folder and open the copied project.arcadia before editing; read its README.md. If you use Save as, also copy evidence/ beside the new entry: Save as copies model source, not external artifact targets.").size(13)); + content = content.push( + button(text("Open water-pressure walkthrough").size(13)) + .padding([11, 15]) + .style(style::nav) + .on_press(Message::Home(crate::home::Event::Walkthrough)), + ); content = content.push( button(text("Open coastal rescue example").size(13)) .padding([11, 15]) @@ -938,6 +1129,7 @@ fn article_view(topic: Topic) -> Element<'static, Message> { bottom: 24., ..Default::default() })) + .id(ARTICLE_SCROLL_ID) .width(Fill) .height(Fill) .into() @@ -974,7 +1166,7 @@ pub fn view(state: &State) -> Element<'_, Message> { button(text("Clear search").size(13)).padding([10, 14]).on_press(Message::Guide(Event::Search(String::new()))), ].spacing(16).max_width(520)).padding(24).width(Fill).into() } else { - // A different article gets a fresh scroll state and starts at its title. + // Topic changes also issue an explicit article-only scroll reset. keyed_column([(state.topic, article_view(state.topic))]) .width(Fill) .height(Fill) @@ -1022,6 +1214,97 @@ pub fn view(state: &State) -> Element<'_, Message> { mod tests { use super::*; + #[derive(Default)] + struct ScrollProbe { + snaps: Vec>>, + } + + impl iced::advanced::widget::operation::Scrollable for ScrollProbe { + fn snap_to( + &mut self, + offset: iced::advanced::widget::operation::scrollable::RelativeOffset>, + ) { + self.snaps.push(offset); + } + fn scroll_to( + &mut self, + _: iced::advanced::widget::operation::scrollable::AbsoluteOffset>, + ) { + panic!("Guide navigation must snap to the article start"); + } + fn scroll_by( + &mut self, + _: iced::advanced::widget::operation::scrollable::AbsoluteOffset, + _: iced::Rectangle, + _: iced::Rectangle, + ) { + panic!("Guide navigation must not use a relative pixel scroll"); + } + } + + #[tokio::test] + async fn guide_topic_navigation_resets_only_article_and_same_topic_search_keeps_scroll() { + use iced::futures::StreamExt; + let mut app = crate::app::App::with_project(oa_core::Project::empty()); + app.page = Page::Guide; + let accepted = app.document.snapshot(); + let revision = app.document.revision(); + for (event, topic, reset) in [ + (Event::Topic(Topic::Scenarios), Topic::Scenarios, true), + ( + Event::Search("sender lifeline".into()), + Topic::Scenarios, + false, + ), + // Native reproduction: related topic after scrolling Scenarios. + (Event::Topic(Topic::Exchanges), Topic::Exchanges, true), + (Event::Search("If-Match 412".into()), Topic::Server, true), + (Event::Search("If-Match".into()), Topic::Server, false), + ( + Event::Search("nonexistent-topic-987".into()), + Topic::Server, + false, + ), + (Event::Search(String::new()), Topic::Server, false), + (Event::Topic(Topic::Server), Topic::Server, false), + ] { + let mut article = ScrollProbe::default(); + let mut navigation = ScrollProbe::default(); + if let Some(mut stream) = + iced_runtime::task::into_stream(app.update(Message::Guide(event))) + { + while let Some(action) = stream.next().await { + let iced_runtime::Action::Widget(mut operation) = action else { + panic!("Guide navigation must not schedule model or external actions"); + }; + for (id, probe) in [ + (ARTICLE_SCROLL_ID, &mut article), + ("guide-topic-navigation", &mut navigation), + ] { + operation.scrollable( + Some(&iced::advanced::widget::Id::new(id)), + iced::Rectangle::default(), + iced::Rectangle::default(), + iced::Vector::new(0., 900.), + probe, + ); + } + } + } + assert_eq!(app.guide.topic, topic); + assert_eq!(article.snaps.len(), usize::from(reset)); + for offset in article.snaps { + assert_eq!(offset.x, Some(0.)); + assert_eq!(offset.y, Some(0.)); + } + assert!(navigation.snaps.is_empty()); + assert_eq!(app.project(), &accepted.decoded.project); + assert_eq!(app.document.bundle(), &accepted.bundle); + assert_eq!(app.document.revision(), revision); + assert!(!app.document.can_undo()); + } + } + #[test] fn search_finds_concepts_in_walkthroughs_and_api_reference() { assert!(search(" ALLOCATED rescue ").contains(&Topic::FirstProject)); diff --git a/crates/oa-desktop/src/home.rs b/crates/oa-desktop/src/home.rs index b335380c..0c5c61d4 100644 --- a/crates/oa-desktop/src/home.rs +++ b/crates/oa-desktop/src/home.rs @@ -21,6 +21,7 @@ pub enum Event { New, Open, Example, + Walkthrough, Guide, Resume, OpenRecent(PathBuf), @@ -43,6 +44,35 @@ pub enum Availability { Unavailable(String), } +/// Resolve the packaged example independently of the process working directory. +pub(crate) fn walkthrough_path() -> Result { + let executable = std::env::current_exe().map_err(|error| error.to_string())?; + let development = + cfg!(debug_assertions).then(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")); + resolve_walkthrough(&executable, development.as_deref()) +} + +fn resolve_walkthrough(executable: &Path, development: Option<&Path>) -> Result { + let relative = Path::new("examples/release1/project.arcadia"); + let packaged = executable + .parent() + .ok_or("Application directory is unavailable.")? + .join(relative); + if packaged.is_file() { + return Ok(packaged); + } + if let Some(root) = development { + let source = root.join(relative); + if source.is_file() { + return Ok(source); + } + } + Err(format!( + "The water-pressure walkthrough is missing. Restore the complete examples/release1 folder beside the application, then try again. Expected {}.", + packaged.display() + )) +} + #[derive(Debug, Clone)] pub struct Recent { pub entry: Entry, @@ -311,6 +341,11 @@ pub fn view<'a>( action("Explore rescue example", Event::Example).style(style::nav), ].spacing(12), ].spacing(18).max_width(940); + content = content.push(container(column![ + text("Restore safe water pressure").size(18), + text("Follow an OA → SA walkthrough with processes, scenarios, states and evidence. Copy the whole examples/release1 folder and open its project.arcadia before editing; the tutorial is README.md. Save as copies model source only, so also copy evidence/ to the new folder if you use it.").size(13), + action("Open water-pressure walkthrough", Event::Walkthrough).style(style::nav), + ].spacing(10)).padding(20).style(style::subtle)); if let Some(warning) = &recovery.warning { content = content.push(text(warning).size(12).color(style::MUTED)); } diff --git a/crates/oa-desktop/src/home/tests.rs b/crates/oa-desktop/src/home/tests.rs index 2dd60a4b..543d9ce9 100644 --- a/crates/oa-desktop/src/home/tests.rs +++ b/crates/oa-desktop/src/home/tests.rs @@ -1,5 +1,42 @@ use super::*; +#[test] +fn walkthrough_uses_packaged_bundle_before_development_and_reports_missing_assets() { + let temp = tempfile::tempdir().unwrap(); + let packaged = temp + .path() + .join("application/examples/release1/project.arcadia"); + let development = temp.path().join("checkout"); + let checkout_entry = development.join("examples/release1/project.arcadia"); + let executable = temp.path().join("application/arcadia-studio.exe"); + fs::create_dir_all(checkout_entry.parent().unwrap()).unwrap(); + fs::write(&checkout_entry, "development source").unwrap(); + assert_eq!( + resolve_walkthrough(&executable, Some(&development)).unwrap(), + checkout_entry + ); + fs::create_dir_all(packaged.parent().unwrap()).unwrap(); + fs::write(&packaged, "packaged source").unwrap(); + assert_eq!( + resolve_walkthrough(&executable, Some(&development)).unwrap(), + packaged + ); + assert_eq!(resolve_walkthrough(&executable, None).unwrap(), packaged); + fs::remove_file(&packaged).unwrap(); + let error = resolve_walkthrough(&executable, None).unwrap_err(); + assert!(error.contains("complete examples/release1 folder")); + assert!( + error + .replace('\\', "/") + .contains(&packaged.display().to_string().replace('\\', "/")), + "{error}" + ); + assert_eq!( + fs::read_to_string(checkout_entry).unwrap(), + "development source" + ); +} + fn model(directory: &Path, name: &str) -> PathBuf { let path = directory.join(name); fs::write(&path, b"model contents must remain untouched").unwrap(); diff --git a/crates/oa-desktop/src/main.rs b/crates/oa-desktop/src/main.rs index 7697af85..43ab49ec 100644 --- a/crates/oa-desktop/src/main.rs +++ b/crates/oa-desktop/src/main.rs @@ -29,6 +29,7 @@ mod source_workspace; mod style; #[cfg(test)] mod tests; +mod view_copy; mod views; mod workflow; diff --git a/crates/oa-desktop/src/model_menu.rs b/crates/oa-desktop/src/model_menu.rs index 65a20cf5..9b4f5fe3 100644 --- a/crates/oa-desktop/src/model_menu.rs +++ b/crates/oa-desktop/src/model_menu.rs @@ -196,6 +196,7 @@ pub fn popup(app: &App) -> Option> { if request.canvas.is_some() && app.active_diagram == Some(id) { entries.extend([ item("View properties", Message::EditProperties(id), true), + item("Duplicate view", Message::DuplicateDiagram(id), true), item("Fit diagram", Message::Fit, true), item( "Arrange elements", @@ -444,6 +445,11 @@ pub fn popup(app: &App) -> Option> { fn relation_actions(kind: Kind) -> &'static [(&'static str, RelationKind, bool)] { match kind { + Kind::Entity | Kind::Actor => &[( + "Configure communication means…", + RelationKind::CommunicationMean, + false, + )], Kind::OperationalMission | Kind::Mission => &[ ( "Exploit capability…", diff --git a/crates/oa-desktop/src/port_workflow.rs b/crates/oa-desktop/src/port_workflow.rs index a66d75af..0dc20d5d 100644 --- a/crates/oa-desktop/src/port_workflow.rs +++ b/crates/oa-desktop/src/port_workflow.rs @@ -226,6 +226,18 @@ impl State { state.preview = state.plan(p); Ok(state) } + pub(crate) fn from_recovery_after_source5_upgrade( + p: &Project, + legacy_base: &str, + value: serde_json::Value, + ) -> Result { + let mut saved: RecoveryState = serde_json::from_value(value).map_err(|e| e.to_string())?; + if saved.version != 1 || saved.base != legacy_base { + return Err("Port workflow recovery does not match the accepted source-5 model identity and revision.".into()); + } + saved.base = transitions::project_fingerprint(p); + Self::from_recovery(p, serde_json::to_value(saved).map_err(|e| e.to_string())?) + } pub fn new(p: &Project, subject: Id, action: Action) -> Result { if !actions(p, subject).contains(&action) { return Err("This port action is unavailable for the selected element.".into()); @@ -802,6 +814,7 @@ mod tests { layer: Layer::Sa, projection: ProjectionKind::Overview, routes: Vec::new(), + view_options: Default::default(), nodes: Default::default(), }, ); diff --git a/crates/oa-desktop/src/recovery.rs b/crates/oa-desktop/src/recovery.rs index bda28fd8..c719ba71 100644 --- a/crates/oa-desktop/src/recovery.rs +++ b/crates/oa-desktop/src/recovery.rs @@ -14,6 +14,9 @@ use std::{ }; const MAX_BYTES: u64 = 256 * 1024 * 1024; +mod source5; +#[cfg(test)] +mod source5_tests; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Snapshot { @@ -126,17 +129,26 @@ impl Snapshot { } let mut document = Document::from_bundle(self.accepted.clone(), LIMITS).map_err(|e| e.to_string())?; - if document.bundle() != &self.accepted { - return Err("Checkpoint accepted source requires migration or identity insertion; it cannot be recovered exactly.".into()); + let legacy_base = source5::upgrade_binding(&self.accepted, &document)?; + let mut saved_workspace = self.workspace; + if legacy_base.is_some() { + saved_workspace.upgrade_clean_buffers(&self.accepted, document.bundle()); } - let workspace = Workspace::from_recovery(self.workspace)?; + let workspace = Workspace::from_recovery(saved_workspace)?; let property = self .property .map(|s| Draft::from_recovery(document.project(), s)) .transpose()?; let workflow = self .workflow - .map(|value| crate::workflow::Workflow::from_recovery(document.project(), value)) + .map(|value| match legacy_base.as_deref() { + Some(base) => crate::workflow::Workflow::from_recovery_after_source5_upgrade( + document.project(), + base, + value, + ), + None => crate::workflow::Workflow::from_recovery(document.project(), value), + }) .transpose()?; let mut behavior = crate::behavior::State::default(); if let Some(saved) = self.behavior { diff --git a/crates/oa-desktop/src/recovery/source5.rs b/crates/oa-desktop/src/recovery/source5.rs new file mode 100644 index 00000000..53b920d8 --- /dev/null +++ b/crates/oa-desktop/src/recovery/source5.rs @@ -0,0 +1,133 @@ +//! Narrow compatibility for private checkpoints written by the source-5 desktop. +use super::*; +use oa_core::{ + Diagram, Project, + language::{self, TokenClass}, +}; +use serde::ser::{SerializeStruct, Serializer}; + +/// Return the historical workflow binding only after proving that migration +/// changed precisely the version token, with no identity or semantic insertion. +pub(super) fn upgrade_binding( + accepted: &SourceBundle, + document: &Document, +) -> Result, String> { + if document.bundle() == accepted { + return Ok(None); + } + let unsupported = || { + "Checkpoint accepted source cannot be recovered exactly; only a lossless source 5 to 6 upgrade is supported. The checkpoint has been retained.".to_owned() + }; + let report = document + .compiled() + .decoded + .migration + .as_ref() + .ok_or_else(unsupported)?; + if (report.from_schema, report.to_schema) != (5, 6) { + return Err(unsupported()); + } + let source = accepted + .files + .get(&accepted.entry) + .ok_or_else(unsupported)?; + let tokens: Vec<_> = language::tokenize(source) + .into_iter() + .filter(|t| !matches!(t.class, TokenClass::Whitespace | TokenClass::Comment)) + .take(3) + .collect(); + if tokens.len() != 3 + || &source[tokens[0].span.start..tokens[0].span.end] != "arcadia" + || tokens[1].class != TokenClass::Number + || &source[tokens[1].span.start..tokens[1].span.end] != "5" + || &source[tokens[2].span.start..tokens[2].span.end] != ";" + { + return Err(unsupported()); + } + let mut expected = accepted.clone(); + expected + .files + .get_mut(&accepted.entry) + .ok_or_else(unsupported)? + .replace_range(tokens[1].span.start..tokens[1].span.end, "6"); + if document.bundle() != &expected { + return Err(unsupported()); + } + // The old source admission rejects all source-6 constructs. Exact token-only + // migration establishes that borrowed records below retain their old shape. + legacy_fingerprint(document.project()).map(Some) +} + +fn legacy_fingerprint(p: &Project) -> Result { + if p.schema_version != 6 + || p.diagrams + .values() + .any(|d| d.view_options != Default::default()) + { + return Err("Checkpoint migration does not have a source-5 model shape.".into()); + } + let bytes = serde_json::to_vec(&Project5(p)).map_err(|e| e.to_string())?; + let hash = bytes.iter().fold(0xcbf29ce484222325u64, |hash, byte| { + (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3) + }); + Ok(format!("{hash:016x}")) +} + +// Preserve the exact historical serde field order (2b2d2bc), including empty +// maps and routes. Serializing through Value would reorder struct fields. +struct Project5<'a>(&'a Project); +impl Serialize for Project5<'_> { + fn serialize(&self, serializer: S) -> Result { + let p = self.0; + let diagrams: std::collections::BTreeMap<_, _> = + p.diagrams.iter().map(|(id, d)| (id, Diagram5(d))).collect(); + let mut s = serializer.serialize_struct("Project", 10)?; + s.serialize_field("schema_version", &5u32)?; + s.serialize_field("id", &p.id)?; + s.serialize_field("alias", &p.alias)?; + s.serialize_field("name", &p.name)?; + s.serialize_field("description", &p.description)?; + s.serialize_field("elements", &p.elements)?; + s.serialize_field("relations", &p.relations)?; + s.serialize_field("diagrams", &diagrams)?; + s.serialize_field("artifact_links", &p.artifact_links)?; + s.serialize_field("traces", &p.traces)?; + s.end() + } +} +struct Diagram5<'a>(&'a Diagram); +impl Serialize for Diagram5<'_> { + fn serialize(&self, serializer: S) -> Result { + let d = self.0; + let mut s = serializer.serialize_struct("Diagram", 8)?; + s.serialize_field("id", &d.id)?; + s.serialize_field("alias", &d.alias)?; + s.serialize_field("name", &d.name)?; + s.serialize_field("description", &d.description)?; + s.serialize_field("layer", &d.layer)?; + s.serialize_field("projection", &d.projection)?; + s.serialize_field("nodes", &d.nodes)?; + s.serialize_field("routes", &d.routes)?; + s.end() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn source5_fingerprint_preserves_historical_field_order_and_diagram_shape() { + // Literal follows the old Project/Diagram derives at 2b2d2bc. The FNV + // value was independently calculated from these UTF-8 bytes with Python. + let bytes = r#"{"schema_version":5,"id":"00000000-0000-0000-0000-000000000001","alias":"p","name":"Legacy","description":"","elements":{},"relations":{},"diagrams":{"00000000-0000-0000-0000-000000000002":{"id":"00000000-0000-0000-0000-000000000002","alias":"v","name":"View","description":"","layer":"Oa","projection":"Overview","nodes":{},"routes":[]}},"artifact_links":{},"traces":{}}"#; + let mut p: Project = serde_json::from_str(bytes).unwrap(); + p.schema_version = 6; + assert_eq!(serde_json::to_string(&Project5(&p)).unwrap(), bytes); + assert_eq!(legacy_fingerprint(&p).unwrap(), "4140e158d5091161"); + assert_ne!( + legacy_fingerprint(&p).unwrap(), + oa_core::transitions::project_fingerprint(&p) + ); + } +} diff --git a/crates/oa-desktop/src/recovery/source5_tests.rs b/crates/oa-desktop/src/recovery/source5_tests.rs new file mode 100644 index 00000000..8004b447 --- /dev/null +++ b/crates/oa-desktop/src/recovery/source5_tests.rs @@ -0,0 +1,296 @@ +//! Source-5 checkpoint compatibility preserves authored bytes and disk contents. +use super::*; + +fn source5_snapshot() -> Snapshot { + let accepted = SourceBundle { + entry: "project.arcadia".into(), + files: [ + ( + "project.arcadia".into(), + "// Preserve 5, 6 and Unicode 雨 in comments.\r\narcadia // header\r\n 5;\r\nproject p \"Recovered source 5\" id \"00000000-0000-4000-8000-000000000001\" {\r\n include \"work.arcadia\";\r\n}\r\n".into(), + ), + ( + "work.arcadia".into(), + "// Exact fragment, including unusual spacing.\r\noa_activity a \"Work\" id \"00000000-0000-4000-8000-000000000002\" {}\r\n".into(), + ), + ].into(), + }; + Snapshot { + version: 1, + id: Id::from_u128(100), + name: "Recovered source 5".into(), + origin: Some(PathBuf::from("original.arcadia")), + modified: 1, + workspace: Workspace::new(&accepted, true).recovery_snapshot(), + accepted, + property: None, + behavior: None, + workflow: None, + selected: Id::from_u128(0x40008000000000000001), + } +} + +#[test] +fn source5_checkpoint_upgrades_only_header_and_clean_editor_baseline() { + let saved = source5_snapshot(); + let mut expected = saved.accepted.clone(); + // Fixture-specific replacement is an independent oracle for the production token span. + expected.files.insert( + expected.entry.clone(), + saved.accepted.files[&saved.accepted.entry].replacen( + "arcadia // header\r\n 5;", + "arcadia // header\r\n 6;", + 1, + ), + ); + let restored = saved.restore().unwrap(); + assert_eq!(restored.document.bundle(), &expected); + let report = restored + .document + .compiled() + .decoded + .migration + .as_ref() + .unwrap(); + assert_eq!((report.from_schema, report.to_schema), (5, 6)); + assert_eq!(restored.workspace.text(), expected.files[&expected.entry]); + assert!(!restored.workspace.dirty()); + assert!(restored.document.dirty()); + assert!(!restored.document.can_undo()); +} + +#[test] +fn source5_checkpoint_preserves_dirty_invalid_text_and_baseline_exactly() { + let mut saved = source5_snapshot(); + let mut workspace = Workspace::new(&saved.accepted, true); + let entry = workspace.entry.clone(); + let text = format!("{}\r\nunfinished {{ 雨", workspace.text()); + workspace.edit(text.clone(), workspace.files[&entry].cursor); + workspace.new_path = "drafts/not-created.arcadia".into(); + saved.workspace = workspace.recovery_snapshot(); + let before = serde_json::to_value(&saved.workspace).unwrap(); + let restored = saved.restore().unwrap(); + assert_eq!( + serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(), + before + ); + assert_eq!(restored.workspace.text(), text); + assert!(restored.workspace.dirty()); + assert!(restored.document.bundle().files[&entry].contains("arcadia // header\r\n 6;")); +} + +#[test] +fn source5_checkpoint_preserves_distinct_unapplied_open_workspace() { + let mut saved = source5_snapshot(); + let broken = SourceBundle { + entry: "broken.arcadia".into(), + files: [("broken.arcadia".into(), "unparsed {\r\n 雨".into())].into(), + }; + saved.workspace = Workspace::new(&broken, false).recovery_snapshot(); + let before = serde_json::to_value(&saved.workspace).unwrap(); + let restored = saved.restore().unwrap(); + assert_eq!( + serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(), + before + ); + assert!(restored.workspace.dirty()); + assert_eq!(restored.document.project().name, "Recovered source 5"); +} + +#[test] +fn source5_checkpoint_upgrade_never_rewrites_checkpoint_or_origin() { + let directory = tempfile::tempdir().unwrap(); + let mut saved = source5_snapshot(); + let origin = directory.path().join("original.arcadia"); + fs::write(&origin, b"External origin bytes stay untouched.").unwrap(); + saved.origin = Some(origin.clone()); + write(directory.path(), &saved).unwrap(); + let before = fs::read(path(directory.path(), saved.id)).unwrap(); + let restored = read(directory.path(), saved.id).unwrap().restore().unwrap(); + assert!(restored.document.dirty()); + assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before); + assert_eq!( + fs::read(&origin).unwrap(), + b"External origin bytes stay untouched." + ); +} + +#[test] +fn source5_checkpoint_missing_identity_is_not_a_lossless_upgrade() { + let directory = tempfile::tempdir().unwrap(); + let mut saved = source5_snapshot(); + saved.accepted.files.insert( + "work.arcadia".into(), + "oa_activity a \"Work\" {}\r\n".into(), + ); + saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); + write(directory.path(), &saved).unwrap(); + let before = fs::read(path(directory.path(), saved.id)).unwrap(); + assert!(read(directory.path(), saved.id).unwrap().restore().is_err()); + assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before); +} + +#[test] +fn source4_checkpoint_remains_explicitly_unsupported_without_touching_disk() { + let directory = tempfile::tempdir().unwrap(); + let mut saved = source5_snapshot(); + let entry = saved.accepted.entry.clone(); + saved.accepted.files.insert( + entry.clone(), + saved.accepted.files[&entry].replacen( + "arcadia // header\r\n 5;", + "arcadia // header\r\n 4;", + 1, + ), + ); + saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); + write(directory.path(), &saved).unwrap(); + let before = fs::read(path(directory.path(), saved.id)).unwrap(); + assert!(read(directory.path(), saved.id).unwrap().restore().is_err()); + assert_eq!(fs::read(path(directory.path(), saved.id)).unwrap(), before); +} + +#[test] +fn source5_checkpoint_workflow_rebinds_only_the_proven_old_revision() { + use crate::workflow::Workflow; + let mut saved = source5_snapshot(); + let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); + let old_base = source5::upgrade_binding(&saved.accepted, &document) + .unwrap() + .unwrap(); + let work = *document.project().elements.keys().next().unwrap(); + let workflow = Workflow::transition(document.project(), work).unwrap(); + let mut raw = workflow.recovery_snapshot().unwrap(); + raw["Transition"]["base"] = old_base.into(); + // This field did not exist in source-5 request snapshots. + raw["Transition"]["request"] + .as_object_mut() + .unwrap() + .remove("communication_endpoints"); + saved.workflow = Some(raw.clone()); + let restored = saved.clone().restore().unwrap(); + let Workflow::Transition(restored_workflow) = restored.workflow.unwrap() else { + panic!() + }; + assert_eq!(restored_workflow.request.sources, vec![work]); + assert_eq!( + restored_workflow.plan.base_fingerprint, + oa_core::transitions::project_fingerprint(restored.document.project()) + ); + assert!(!serde_json::to_string(&raw).unwrap().contains("commands")); + raw["Transition"]["base"] = "different accepted revision".into(); + saved.workflow = Some(raw); + assert!(saved.restore().err().unwrap().contains("revision")); +} + +#[test] +fn source5_checkpoint_property_preserves_invalid_raw_input() { + let mut saved = source5_snapshot(); + // Add a historical diagram and placement, with stable explicit identities. + saved.accepted.files.get_mut("work.arcadia").unwrap().push_str( + "diagram v \"View\" id \"00000000-0000-4000-8000-000000000003\" {}\r\nnode placed id \"00000000-0000-4000-8000-000000000004\" { diagram @v; element @a; }\r\n"); + saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); + let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); + let node = *document + .project() + .diagrams + .values() + .next() + .unwrap() + .nodes + .keys() + .next() + .unwrap(); + let mut draft = Draft::from_project(document.project(), node).unwrap(); + draft.input("/position/x", "-".into()); + saved.property = Some(draft.recovery_snapshot()); + let restored = saved.restore().unwrap(); + let draft = restored.property.unwrap(); + assert_eq!(draft.inputs["/position/x"].raw, "-"); + assert!(draft.materialize().is_err()); + assert!(draft.dirty()); +} + +#[test] +fn source5_checkpoint_port_draft_replans_and_rejects_stale_base() { + use crate::{port_workflow, workflow::Workflow}; + let mut saved = source5_snapshot(); + saved + .accepted + .files + .get_mut("work.arcadia") + .unwrap() + .push_str("sa_function f \"Function\" id \"00000000-0000-4000-8000-000000000005\" {}\r\n"); + saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); + let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); + let p = document.project(); + let f = p + .elements + .values() + .find(|e| e.kind() == oa_core::Kind::Function) + .unwrap() + .id; + let old_base = source5::upgrade_binding(&saved.accepted, &document) + .unwrap() + .unwrap(); + let state = port_workflow::State::new(p, f, port_workflow::Action::Input).unwrap(); + let mut raw = state.recovery_snapshot().unwrap(); + raw["name"] = " raw port name ".into(); + raw["base"] = old_base.into(); + saved.workflow = Some(serde_json::json!({"Ports": raw})); + let restored = saved.clone().restore().unwrap(); + let Workflow::Ports(state) = restored.workflow.unwrap() else { + panic!() + }; + let restored_raw = state.recovery_snapshot().unwrap(); + assert_eq!(restored_raw["name"], " raw port name "); + assert_eq!( + restored_raw["base"], + oa_core::transitions::project_fingerprint(restored.document.project()) + ); + saved.workflow.as_mut().unwrap()["Ports"]["base"] = "stale".into(); + assert!(saved.restore().err().unwrap().contains("revision")); +} + +#[test] +fn source5_checkpoint_old_diagram_property_shape_remains_editable() { + let mut saved = source5_snapshot(); + saved + .accepted + .files + .get_mut("work.arcadia") + .unwrap() + .push_str("diagram v \"View\" id \"00000000-0000-4000-8000-000000000003\" {}\r\n"); + saved.workspace = Workspace::new(&saved.accepted, true).recovery_snapshot(); + let document = Document::from_bundle(saved.accepted.clone(), LIMITS).unwrap(); + let diagram = *document.project().diagrams.keys().next().unwrap(); + let mut draft = Draft::from_project(document.project(), diagram).unwrap(); + draft.input("/name", " raw view name ".into()); + let mut raw = serde_json::to_value(draft.recovery_snapshot()).unwrap(); + raw["value"].as_object_mut().unwrap().remove("view_options"); + raw["inputs"] + .as_object_mut() + .unwrap() + .retain(|key, _| !key.starts_with("/view_options")); + saved.property = Some(serde_json::from_value(raw).unwrap()); + let restored = saved.restore().unwrap(); + let draft = restored.property.unwrap(); + assert_eq!(draft.inputs["/name"].raw, " raw view name "); + assert!(draft.materialize().is_ok()); + assert!(draft.dirty()); +} + +#[test] +fn source5_checkpoint_does_not_refresh_unrelated_clean_buffer_baseline() { + let mut saved = source5_snapshot(); + let mut raw = serde_json::to_value(&saved.workspace).unwrap(); + // A clean buffer retained from a different accepted source must not be + // mistaken for the source-5 entry merely because its path matches. + raw["files"]["project.arcadia"] = serde_json::json!(["different source", "different source"]); + saved.workspace = serde_json::from_value(raw.clone()).unwrap(); + let restored = saved.restore().unwrap(); + assert_eq!( + serde_json::to_value(restored.workspace.recovery_snapshot()).unwrap(), + raw + ); +} diff --git a/crates/oa-desktop/src/semantic_review.rs b/crates/oa-desktop/src/semantic_review.rs index 0a6c6852..1f5ce1b3 100644 --- a/crates/oa-desktop/src/semantic_review.rs +++ b/crates/oa-desktop/src/semantic_review.rs @@ -1,4 +1,4 @@ -//! Deliberate semantic review of one existing trace; drafts never mutate the model. +//! Atomic semantic review session with independent, stably addressed row drafts. use crate::{ app::{Choice, Message}, forms::Draft, @@ -8,6 +8,18 @@ use oa_core::{transitions::*, *}; use serde_json::Value; use std::collections::{BTreeMap, BTreeSet}; +mod artifacts; +#[cfg(test)] +mod artifacts_recovery_falsifier_tests; +#[cfg(test)] +mod artifacts_tests; +mod artifacts_view; +mod attachments; +mod attachments_view; +mod batch; +#[cfg(test)] +mod batch_tests; +mod compose; mod constructors; mod history; mod keyed; @@ -15,6 +27,7 @@ mod keyed_rows; mod message_kind; mod ordered; mod recovery; +mod row; mod scalar; mod view; pub(crate) use view::summary; @@ -36,13 +49,17 @@ pub enum Tab { Facts, References, Members, + Attachments, + Artifacts, History, Impacts, } #[derive(Debug, Clone)] -pub enum Event { +pub enum RowEvent { + #[cfg(test)] Trace(Choice), + #[cfg(test)] Direction(TransitionDirection), Select(FacetSelector), Decide(FacetSelector, Option), @@ -53,12 +70,15 @@ pub enum Event { Array(String, Option, i32), Append(String, Value), ValuePath(String, crate::data_values::FieldPathKey), + ValueBrowse(String, Option, usize), + ReferenceBrowse(String, String, usize), Tab(Tab), ShowUnchanged(bool), ReferenceOa(Choice), ReferenceSa(Choice), ReferenceSearch(String), AddReference, + RestoreReviewedReferences, RemoveReference(Id), AcceptImpact(usize, bool), Preview, @@ -71,6 +91,8 @@ pub enum Event { LiteralSide(Layer, bool), EditPair(FacetSelector), StageMember, + Attachment(attachments::Event), + Artifact(artifacts::Event), DropStaged(FacetSelector), AnnotationAddress(Layer, Option), AnnotationBrowse(Layer, Option, usize), @@ -82,824 +104,5 @@ pub enum Event { OrderReconcile(OrderedCollection), } -pub struct State { - base_identity: Id, - base_fingerprint: String, - pub anchor: Id, - pub trace: Option, - pub direction: TransitionDirection, - pub facts: Vec, - pub selected: Option, - pub decisions: BTreeMap, - pub references: BTreeMap, - pub resets: BTreeSet, - pub staged: BTreeSet, - pub presence: BTreeMap, - pub row_drafts: BTreeMap, - pub order_drafts: BTreeMap>, - pub annotation_oa: keyed_rows::AddressDraft, - pub annotation_sa: keyed_rows::AddressDraft, - pub member_family: keyed::Family, - pub member_oa: Option, - pub member_sa: Option, - pub literal: String, - pub literal_oa: bool, - pub literal_sa: bool, - pub draft: Option, - // Each closed constructor has an independent draft, including nested kinds. - // Scalar and sibling drafts survive decision and constructor changes. - pub group_drafts: BTreeMap, - pub plan: Option, - pub proposed: Option, - pub error: Option, - pub tab: Tab, - pub show_unchanged: bool, - pub reference_oa: Option, - pub reference_sa: Option, - pub reference_search: String, - accepted_impacts: Vec, -} - -impl State { - pub fn new(project: &Project, selected: Id) -> Result { - let explicit = project.traces.get(&selected); - let anchor = explicit.map_or(selected, |trace| trace.oa); - let layer = project - .layer_of(anchor) - .ok_or("Select an OA or SA object, relationship or trace to review.")?; - let choices: Vec<_> = project - .traces - .values() - .filter(|t| t.oa == anchor || t.sa == anchor) - .collect(); - if choices.is_empty() { - return Err("This object has no trace. Create a counterpart or link existing counterparts first.".into()); - } - let trace = explicit - .map(|t| t.id) - .or_else(|| (choices.len() == 1).then(|| choices[0].id)); - let mut state = Self { - base_identity: project.id, - base_fingerprint: project_fingerprint(project), - anchor, - trace, - direction: if layer == Layer::Oa { - TransitionDirection::OaToSa - } else { - TransitionDirection::SaToOa - }, - facts: vec![], - selected: None, - decisions: BTreeMap::new(), - references: BTreeMap::new(), - resets: BTreeSet::new(), - staged: BTreeSet::new(), - presence: BTreeMap::new(), - row_drafts: BTreeMap::new(), - order_drafts: BTreeMap::new(), - annotation_oa: Default::default(), - annotation_sa: Default::default(), - member_family: keyed::Family::Literal, - member_oa: None, - member_sa: None, - literal: String::new(), - literal_oa: true, - literal_sa: true, - draft: None, - group_drafts: BTreeMap::new(), - plan: None, - proposed: None, - error: None, - tab: Tab::Facts, - show_unchanged: false, - reference_oa: None, - reference_sa: None, - reference_search: String::new(), - accepted_impacts: vec![], - }; - state.load_trace(project); - Ok(state) - } - - pub fn target(&self, project: &Project) -> Option { - let trace = project.traces.get(&self.trace?)?; - Some(if self.direction == TransitionDirection::OaToSa { - trace.sa - } else { - trace.oa - }) - } - - fn load_trace(&mut self, project: &Project) { - self.facts.clear(); - self.decisions.clear(); - self.resets.clear(); - self.staged.clear(); - self.presence.clear(); - self.row_drafts.clear(); - self.order_drafts.clear(); - self.annotation_oa = Default::default(); - self.annotation_sa = Default::default(); - self.member_oa = None; - self.member_sa = None; - self.literal.clear(); - self.literal_oa = true; - self.literal_sa = true; - self.selected = None; - self.accepted_impacts.clear(); - self.references.clear(); - self.draft = self - .target(project) - .and_then(|id| Draft::from_project(project, id)); - self.group_drafts.clear(); - if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { - for facet in constructors::selectors() { - if constructors::available(project, trace, &facet) - && let Some(mut draft) = self - .target(project) - .and_then(|id| Draft::from_project(project, id)) - { - draft.semantic_groups = true; - self.group_drafts.insert(facet, draft); - } - } - } - if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { - self.references.insert(trace.oa, trace.sa); - } - self.member_family = self - .member_families(project) - .first() - .copied() - .unwrap_or(keyed::Family::Literal); - self.refresh(project); - } - - pub fn update(&mut self, project: &Project, event: Event) { - let mut replan = true; - let result = match event { - Event::Trace(choice) => { - if !project - .traces - .get(&choice.id) - .is_some_and(|t| t.oa == self.anchor || t.sa == self.anchor) - { - Err("Choose one of this object's existing traces.".into()) - } else { - self.trace = Some(choice.id); - self.load_trace(project); - return; - } - } - Event::Direction(direction) => { - self.direction = direction; - self.load_trace(project); - return; - } - Event::Select(facet) => { - self.selected = Some(facet); - self.tab = Tab::Facts; - replan = false; - Ok(()) - } - Event::Decide(facet, decision) => { - if let Some(decision) = decision { - if !self.editable(project, &facet) - && !(decision == SemanticDecision::KeepTarget - && self.can_acknowledge_constructor(project, &facet)) - { - Err("This fact needs the structured review workflow; its history remains unchanged.".into()) - } else { - if let FacetSelector::Order(which) = facet - && decision == SemanticDecision::Candidate - { - if let Err(error) = self.initialize_order(project, which) { - self.error = Some(error); - return; - } - } else if keyed_rows::supported(&facet) { - self.initialize_row(project, &facet); - } else if keyed::supported(&facet) { - let target_presence = self - .effective_trace(project) - .and_then(|t| capture_facet(project, t.oa, t.sa, &facet).ok()) - .and_then(|f| scalar::value(&f, self.direction.target_layer())) - .and_then(|v| v.as_bool()) - .unwrap_or(false); - self.presence - .entry(facet.clone()) - .or_insert(target_presence); - } - self.decisions.insert(facet, decision); - Ok(()) - } - } else { - self.decisions.remove(&facet); - Ok(()) - } - } - Event::Input(path, value) => self.edit(project, &path, |d| { - d.input(&path, value); - Ok(()) - }), - Event::Text(path, action) => self.edit(project, &path, |d| { - d.edit_text(&path, action); - Ok(()) - }), - Event::Shape(path, value) => { - self.edit(project, &path, |d| d.shape_scoped(&path, value)) - } - Event::Variant(path, value) => { - self.edit(project, &path, |d| d.variant_scoped(&path, value)) - } - Event::Array(path, index, direction) => { - self.edit(project, &path, |d| d.array_scoped(&path, index, direction)) - } - Event::Append(path, value) => { - self.edit(project, &path, |d| d.append_scoped(&path, value)) - } - Event::ValuePath(path, key) => { - self.edit(project, &path, |d| d.value_path_scoped(&path, key)) - } - Event::Tab(tab) => { - self.tab = tab; - replan = false; - Ok(()) - } - Event::ShowUnchanged(show) => { - self.show_unchanged = show; - replan = false; - Ok(()) - } - Event::ReferenceOa(choice) => { - self.reference_oa = Some(choice); - replan = false; - Ok(()) - } - Event::ReferenceSa(choice) => { - self.reference_sa = Some(choice); - replan = false; - Ok(()) - } - Event::ReferenceSearch(value) => { - self.reference_search = value; - replan = false; - Ok(()) - } - Event::AddReference => match (&self.reference_oa, &self.reference_sa) { - (Some(oa), Some(sa)) => { - self.references.insert(oa.id, sa.id); - Ok(()) - } - _ => Err("Choose both an Operational and a System reference.".into()), - }, - Event::RemoveReference(id) => { - self.references.remove(&id); - Ok(()) - } - Event::AcceptImpact(index, accepted) => { - if let Some(impact) = self - .plan - .as_ref() - .and_then(|p| p.impacts.get(index)) - .cloned() - { - self.accepted_impacts.retain(|i| i.object != impact.object); - if accepted { - self.accepted_impacts.push(impact); - } - } - self.refresh(project); - return; - } - Event::Preview => Ok(()), - Event::OrderMove(which, index, delta) => self.move_order(which, index, delta), - Event::OrderReconcile(which) => self.reconcile_order(project, which), - Event::Reset(facet, reset) => self.reset_history(project, facet, reset), - Event::Presence(facet, present) => { - if keyed::supported(&facet) - && self.decisions.get(&facet) == Some(&SemanticDecision::Candidate) - { - self.presence.insert(facet, present); - Ok(()) - } else { - Err("Choose Set final value for this member before editing presence.".into()) - } - } - Event::MemberFamily(family) => { - self.member_family = family; - replan = false; - Ok(()) - } - Event::MemberOa(choice) => { - self.member_oa = choice; - replan = false; - Ok(()) - } - Event::MemberSa(choice) => { - self.member_sa = choice; - replan = false; - Ok(()) - } - Event::Literal(value) => { - self.literal = value; - replan = false; - Ok(()) - } - Event::LiteralSide(layer, present) => { - if layer == Layer::Oa { - self.literal_oa = present; - } else { - self.literal_sa = present; - } - replan = false; - Ok(()) - } - Event::EditPair(facet) => { - replan = false; - self.edit_pair(project, facet) - } - Event::StageMember => self.stage_member(project), - Event::DropStaged(facet) => { - if self.staged.remove(&facet) { - self.decisions.remove(&facet); - } - Ok(()) - } - Event::AnnotationAddress(layer, key) => { - self.annotation_address_mut(layer).selected = key; - replan = false; - Ok(()) - } - Event::AnnotationBrowse(layer, scope, page) => { - let address = self.annotation_address_mut(layer); - address.scope = scope; - address.page = page; - replan = false; - Ok(()) - } - #[cfg(test)] - Event::RowExpression(facet, expression) => self.edit_row(&facet, |draft| { - draft.editor = text_editor::Content::with_text(&expression); - draft.expression = expression; - }), - Event::RowText(facet, action) => self.edit_row(&facet, |draft| { - draft.editor.perform(action); - draft.expression = draft.editor.text(); - }), - Event::RowLocal(facet, local) => self.edit_row(&facet, |draft| { - draft.local = Some(local); - }), - }; - if let Err(error) = result { - self.error = Some(error); - self.plan = None; - self.proposed = None; - return; - } - if replan { - self.accepted_impacts.clear(); - self.refresh(project); - } - } - - fn edit( - &mut self, - project: &Project, - path: &str, - action: impl FnOnce(&mut Draft) -> Result<(), String>, - ) -> Result<(), String> { - let facet = self.selected.as_ref().ok_or("Choose a property first.")?; - if self.decisions.get(facet) != Some(&SemanticDecision::Candidate) { - return Err("Choose Set final value before editing this property.".into()); - } - let roots = self - .target(project) - .and_then(|id| constructors::roots(project, id, facet)) - .ok_or("Selected property is unavailable.")?; - if !roots - .iter() - .any(|root| path == root || path.starts_with(&format!("{root}/"))) - { - return Err("Only the selected review property can change in this draft.".into()); - } - let draft = if scalar::grouped(facet) { - self.group_drafts.get_mut(facet) - } else { - self.draft.as_mut() - } - .ok_or("Choose an existing target.")?; - // The scoped view sends only the selected property or closed group roots. - // Protected identity and top-level body/perspective controls are never exposed. - if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" { - return Err("Semantic candidates retain identity, alias and perspective.".into()); - } - action(draft) - } - - pub fn editable(&self, project: &Project, facet: &FacetSelector) -> bool { - if keyed::supported(facet) || matches!(facet, FacetSelector::Order(_)) { - return self - .effective_trace(project) - .is_some_and(|trace| capture_facet(project, trace.oa, trace.sa, facet).is_ok()); - } - self.target(project) - .and_then(|id| scalar::binding(project, id, facet)) - .is_some() - && (!scalar::grouped(facet) - || (self.group_drafts.contains_key(facet) - && self - .trace - .and_then(|id| project.traces.get(&id)) - .is_some_and(|trace| constructors::available(project, trace, facet)))) - } - - fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> { - if scalar::grouped(facet) { - self.group_drafts.get(facet) - } else { - self.draft.as_ref() - } - } - - pub fn can_acknowledge_constructor(&self, project: &Project, facet: &FacetSelector) -> bool { - if !scalar::grouped(facet) { - return false; - } - let Some(trace) = self.effective_trace(project) else { - return false; - }; - if trace - .baseline - .as_ref() - .and_then(|b| b.semantic.as_ref()) - .is_some_and(|b| b.facets.iter().any(|f| f.selector() == *facet)) - { - return false; - } - capture_facet(project, trace.oa, trace.sa, facet) - .ok() - .is_some_and(|current| { - scalar::value(¤t, Layer::Oa).is_some() - && scalar::observation(¤t, Layer::Oa) - == scalar::observation(¤t, Layer::Sa) - }) - } - - fn request(&self, project: &Project) -> Result { - let trace = self - .trace - .and_then(|id| project.traces.get(&id)) - .ok_or("Choose the trace whose review history you want to update.")?; - let (source, target) = if self.direction == TransitionDirection::OaToSa { - (trace.oa, trace.sa) - } else { - (trace.sa, trace.oa) - }; - let mut transition = TransitionRequest::new(self.direction, vec![source]); - transition.mappings.push(TransitionMapping { - source, - target: Some(target), - target_kind: None, - }); - let mut request = SemanticTransitionRequest::new(transition); - request.traces.insert(source, trace.id); - request.references = self.references.clone(); - request.reviews.push(SemanticReviewRequest { - source, - resolutions: self - .decisions - .iter() - .map(|(facet, decision)| FacetResolution { - facet: facet.clone(), - decision: *decision, - }) - .collect(), - reset: self.resets.iter().cloned().collect(), - }); - request.accepted_impacts = self.accepted_impacts.clone(); - if self - .decisions - .values() - .any(|d| *d == SemanticDecision::Candidate) - { - let final_value = self.combined_value(project, false)?; - let candidate = if project.elements.contains_key(&target) { - SemanticRecord::Element( - serde_json::from_value(final_value) - .map_err(|e| format!("Final target value: {e}"))?, - ) - } else { - SemanticRecord::Relation( - serde_json::from_value(final_value) - .map_err(|e| format!("Final target value: {e}"))?, - ) - }; - request.candidates.push(candidate); - } - Ok(request) - } - - /// Compose selected values for both the candidate and dependent form - /// controls. Incomplete inputs retain their stored shape only for controls; - /// request construction always requires every selected input to parse. - fn combined_value(&self, project: &Project, form_context: bool) -> Result { - self.combine(project, form_context, false) - } - - fn combine( - &self, - project: &Project, - form_context: bool, - skip_orders: bool, - ) -> Result { - let trace = self.effective_trace(project).ok_or("Choose a trace.")?; - let (source, target) = if self.direction == TransitionDirection::OaToSa { - (trace.oa, trace.sa) - } else { - (trace.sa, trace.oa) - }; - let mut final_value = Draft::from_project(project, target) - .ok_or("Target has no editable record.")? - .value; - let mut map = BTreeMap::new(); - for (oa, sa) in &self.references { - let (from, to) = if self.direction == TransitionDirection::OaToSa { - (*oa, *sa) - } else { - (*sa, *oa) - }; - if map.insert(from, to).is_some_and(|old| old != to) { - return Err("Reverse reference correspondence is ambiguous.".into()); - } - } - map.insert(source, target); - let original = final_value.clone(); - let mut group_candidates = vec![]; - // Outer groups precede nested groups and leaves. The core rejects actual - // overlapping replacement scopes; this order also supports first-equal - // discriminator acknowledgment together with explicit descendant values. - let mut decisions: Vec<_> = self.decisions.iter().collect(); - decisions.sort_by_key(|(facet, _)| { - let group = match facet { - FacetSelector::Occurrence(OccurrenceFacet::Constructor) => 0, - facet if scalar::grouped(facet) => 1, - FacetSelector::Order(_) => 3, - _ => 2, - }; - ( - group, - keyed_rows::target_selector(facet, self.direction.target_layer()), - ) - }); - for (facet, decision) in decisions { - if *decision == SemanticDecision::KeepTarget { - continue; - } - if let FacetSelector::Order(which) = facet { - if !skip_orders { - // Invalid order drafts remain visible while dependent controls use the - // surviving membership; request construction still rejects them. - if let Err(error) = - self.project_order(project, &trace, &mut final_value, *which, *decision) - && !form_context - { - return Err(error); - } - } - continue; - } - if matches!(facet, FacetSelector::Member { collection, .. } if ordered::ordered(*collection).is_some()) - { - self.project_ordered_member( - project, - &trace, - &mut final_value, - facet, - *decision, - form_context, - )?; - continue; - } - if keyed::supported(facet) { - self.keyed_value(project, &trace, &mut final_value, facet, *decision)?; - continue; - } - let roots = constructors::roots(project, target, facet) - .ok_or("Selected property is not applicable to the target.")?; - let supplied = match decision { - SemanticDecision::KeepTarget => continue, - SemanticDecision::Candidate => { - let draft = self - .candidate_draft(facet) - .ok_or("Target draft is unavailable.")?; - match draft.materialize_scopes(&roots) { - Ok(value) => value, - Err(_) if form_context => draft.value.clone(), - Err(error) => return Err(error), - } - } - SemanticDecision::TakeSource if scalar::grouped(facet) => { - if self.can_acknowledge_constructor(project, facet) { - original.clone() - } else { - constructors::take_value(project, &trace, self.direction, facet, &map)? - } - } - SemanticDecision::TakeSource => { - let fact = capture_facet(project, trace.oa, trace.sa, facet)?; - let value = scalar::source_value(&fact, self.direction.source_layer(), &map)?; - let mut supplied = original.clone(); - *supplied - .pointer_mut(&roots[0]) - .ok_or("Source property is unavailable.")? = value; - supplied - } - }; - for path in roots { - let value = supplied - .pointer(&path) - .cloned() - .ok_or("Candidate property is no longer available.")?; - if scalar::grouped(facet) && *decision == SemanticDecision::Candidate { - group_candidates.push((path.clone(), value.clone())); - } - *final_value.pointer_mut(&path).ok_or("Final target property is unavailable; resolve the overlapping constructor decision.")? = value; - } - } - if !form_context { - for (path, authored) in group_candidates { - if !original - .pointer(&path) - .zip(final_value.pointer(&path)) - .is_some_and(|(original, final_value)| { - constructors::authored_agrees(original, &authored, final_value) - }) - { - return Err("The constructor candidate and descendant decision propose different final values. Keep the kind or make their final values agree; both drafts are retained.".into()); - } - } - } - Ok(final_value) - } - - pub fn refresh(&mut self, project: &Project) { - self.plan = None; - self.proposed = None; - self.error = None; - let context = self.combined_value(project, true).ok(); - if let Some(draft) = &mut self.draft { - draft.set_context(context.clone()); - } - for draft in self.group_drafts.values_mut() { - draft.set_context(context.clone()); - } - let Some(trace) = self.effective_trace(project) else { - self.error = Some( - "Several traces are available. Choose the correspondence and review history above." - .into(), - ); - return; - }; - match semantic_facets(project, &trace, &self.references) { - Ok(mut facts) => { - // Explicit addresses survive catalog normalization, pairing changes and resets. - for selector in self.staged.iter().chain(self.decisions.keys()) { - if !facts.iter().any(|f| &f.selector == selector) { - match capture_facet(project, trace.oa, trace.sa, selector) { - Ok(current) => { - let previous = trace - .baseline - .as_ref() - .and_then(|b| b.semantic.as_ref()) - .and_then(|b| { - b.facets.iter().find(|f| f.selector() == *selector) - }); - facts.push(FacetDrift { - selector: selector.clone(), - drift: facet_drift(¤t, previous), - current, - }); - } - Err(error) => { - self.error = Some(error); - return; - } - } - } - } - facts.sort_by(|a, b| a.selector.cmp(&b.selector)); - self.facts = facts; - } - Err(error) => { - self.error = Some(error); - return; - } - } - if self - .selected - .as_ref() - .is_none_or(|s| !self.facts.iter().any(|f| &f.selector == s)) - { - self.selected = self - .facts - .iter() - .find(|f| f.drift != Drift::Unchanged) - .or(self.facts.first()) - .map(|f| f.selector.clone()); - } - let request = match self.request(project) { - Ok(request) => request, - Err(error) => { - self.error = Some(error); - return; - } - }; - match plan_semantic_transition(project, request) { - Ok(plan) => { - if plan.conflicts.is_empty() { - match prepare_semantic_transition(project, &plan) { - Ok(prepared) => self.proposed = Some(prepared.project), - Err(error) => self.error = Some(error.to_string()), - } - } - self.plan = Some(plan); - } - Err(error) => self.error = Some(error.to_string()), - } - } - - pub fn command(&mut self, project: &Project) -> Result { - if self.decisions.is_empty() && self.resets.is_empty() { - return Err( - "Choose at least one property to review or an exact history observation to reset." - .into(), - ); - } - if let Some(error) = &self.error { - return Err(error.clone()); - } - let plan = self - .plan - .as_ref() - .ok_or("Refresh the review preview before applying.")?; - prepare_semantic_transition(project, plan).map_err(|e| e.to_string())?; - Ok(Command::Batch(plan.commands.clone())) - } - - pub fn can_apply(&self) -> bool { - (!self.decisions.is_empty() || !self.resets.is_empty()) - && self.error.is_none() - && self - .plan - .as_ref() - .is_some_and(|p| p.conflicts.is_empty() && p.prepared_fingerprint.is_some()) - } - - pub fn all_references(&self, project: &Project, layer: Layer) -> Vec { - let historical: BTreeSet<_> = self - .facts - .iter() - .flat_map(|f| scalar::references(&f.current)) - .chain( - self.trace - .and_then(|id| project.traces.get(&id)) - .and_then(|t| t.baseline.as_ref()) - .and_then(|b| b.semantic.as_ref()) - .into_iter() - .flat_map(|b| b.facets.iter().flat_map(scalar::references)), - ) - .collect(); - let mut ids: BTreeSet<_> = project - .elements - .keys() - .chain(project.relations.keys()) - .chain(project.artifact_links.keys()) - .filter(|id| project.layer_of(**id) == Some(layer)) - .copied() - .collect(); - ids.extend(historical.into_iter().filter(|id| !project.contains(*id))); - let search = self.reference_search.to_lowercase(); - let mut choices: Vec<_> = ids - .into_iter() - .map(|id| view::choice(project, id)) - .filter(|c| search.is_empty() || c.label.to_lowercase().contains(&search)) - .collect(); - choices.sort_by(|a, b| a.label.cmp(&b.label)); - choices - } -} - -pub fn form_event(message: Message) -> Message { - let event = match message { - Message::Field(path, value) => Event::Input(path, value), - Message::FieldText(path, action) => Event::Text(path, action), - Message::FieldShape(path, value) => Event::Shape(path, value), - Message::FieldVariant(path, value) => Event::Variant(path, value), - Message::FieldArray(path, index, direction) => Event::Array(path, index, direction), - Message::FieldAppend(path, value) => Event::Append(path, value), - Message::FieldValuePath(path, key) => Event::ValuePath(path, key), - _ => Event::Preview, - }; - Message::Workflow(crate::workflow::Event::Semantic(event)) -} +pub use batch::{Event, RowAddress, State}; +pub use row::ReviewRow; diff --git a/crates/oa-desktop/src/semantic_review/artifacts.rs b/crates/oa-desktop/src/semantic_review/artifacts.rs new file mode 100644 index 00000000..4a14063f --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/artifacts.rs @@ -0,0 +1,615 @@ +//! Typed artifact lifecycle drafts; all authored values belong to an exact pair/facet. +use super::*; +use serde::{Deserialize, Serialize}; +pub(super) const METADATA: [ArtifactFacet; 7] = [ + ArtifactFacet::Name, + ArtifactFacet::Description, + ArtifactFacet::Owner, + ArtifactFacet::Relation, + ArtifactFacet::Uri, + ArtifactFacet::Revision, + ArtifactFacet::Fragment, +]; +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Raw { + Text(String), + Owner(String), + Relation(ArtifactRelation), + Optional { present: bool, text: String }, +} +#[derive(Debug, Clone)] +pub enum Event { + Pick(Layer, Option), + Search(String), + Stage, + Reserve, + Input(FacetSelector, Raw), + Description(FacetSelector, text_editor::Action), +} +#[derive(Default)] +pub struct Editor { + pub oa: Option, + pub sa: Option, + pub search: String, + pub seeds: BTreeMap, + pub drafts: BTreeMap, + pub descriptions: BTreeMap, +} +#[derive(Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Saved { + oa: Option, + sa: Option, + seeds: Vec, + drafts: Vec<(FacetSelector, Raw)>, +} +impl Saved { + pub fn is_empty(&self) -> bool { + self.oa.is_none() && self.sa.is_none() && self.seeds.is_empty() && self.drafts.is_empty() + } +} +pub struct Proposal { + // The final selector addresses the actual mutation slot; the last selector + // retains the reviewed facet solely for conflict diagnostics. + pub slots: Vec<((Id, FacetSelector), Value, FacetSelector)>, + pub creations: Vec, + pub candidates: BTreeSet, +} +pub(super) fn parts(facet: &FacetSelector) -> Option<(&KeyPair, ArtifactFacet)> { + if let FacetSelector::Artifact { keys, facet } = facet { + Some((keys, *facet)) + } else { + None + } +} +pub(super) fn supported(facet: &FacetSelector) -> bool { + parts(facet).is_some() +} +pub(super) fn selector(keys: &KeyPair, facet: ArtifactFacet) -> FacetSelector { + FacetSelector::Artifact { + keys: keys.clone(), + facet, + } +} +pub(super) fn key(keys: &KeyPair, layer: Layer) -> Option { + if layer == Layer::Oa { keys.oa } else { keys.sa } +} +pub(super) fn field(facet: ArtifactFacet) -> Option<&'static str> { + Some(match facet { + ArtifactFacet::Presence => return None, + ArtifactFacet::Name => "name", + ArtifactFacet::Description => "description", + ArtifactFacet::Owner => "owner", + ArtifactFacet::Relation => "relation", + ArtifactFacet::Uri => "uri", + ArtifactFacet::Revision => "revision", + ArtifactFacet::Fragment => "fragment", + }) +} +pub(super) fn slot(id: Id, facet: ArtifactFacet) -> (Id, FacetSelector) { + ( + id, + selector( + &KeyPair { + oa: None, + sa: Some(id), + }, + if facet == ArtifactFacet::Presence { + ArtifactFacet::Owner + } else { + facet + }, + ), + ) +} +pub(super) fn observation(record: Option<&ArtifactLink>, facet: ArtifactFacet) -> Value { + match record { + None => Value::String("NotApplicable".into()), + Some(record) => { + serde_json::json!({"Value":serde_json::to_value(record).expect("Artifact")[field(facet).unwrap_or("owner")]}) + } + } +} +pub(super) fn raw(record: &ArtifactLink, facet: ArtifactFacet) -> Option { + Some(match facet { + ArtifactFacet::Presence => return None, + ArtifactFacet::Name => Raw::Text(record.name.clone()), + ArtifactFacet::Description => Raw::Text(record.description.clone()), + ArtifactFacet::Uri => Raw::Text(record.uri.clone()), + ArtifactFacet::Owner => Raw::Owner(record.owner.to_string()), + ArtifactFacet::Relation => Raw::Relation(record.relation), + ArtifactFacet::Revision => Raw::Optional { + present: record.revision.is_some(), + text: record.revision.clone().unwrap_or_default(), + }, + ArtifactFacet::Fragment => Raw::Optional { + present: record.fragment.is_some(), + text: record.fragment.clone().unwrap_or_default(), + }, + }) +} +fn compatible(facet: ArtifactFacet, raw: &Raw) -> bool { + matches!( + (facet, raw), + ( + ArtifactFacet::Name | ArtifactFacet::Description | ArtifactFacet::Uri, + Raw::Text(_) + ) | (ArtifactFacet::Owner, Raw::Owner(_)) + | (ArtifactFacet::Relation, Raw::Relation(_)) + | ( + ArtifactFacet::Revision | ArtifactFacet::Fragment, + Raw::Optional { .. } + ) + ) +} +fn write(record: &mut ArtifactLink, facet: ArtifactFacet, raw: &Raw) -> Result<(), String> { + match (facet, raw) { + (ArtifactFacet::Name, Raw::Text(v)) => record.name = v.clone(), + (ArtifactFacet::Description, Raw::Text(v)) => record.description = v.clone(), + (ArtifactFacet::Uri, Raw::Text(v)) => record.uri = v.clone(), + (ArtifactFacet::Owner, Raw::Owner(v)) => { + record.owner = v + .parse() + .map_err(|_| "Choose a valid explicit artifact owner identity.")? + } + (ArtifactFacet::Relation, Raw::Relation(v)) => record.relation = *v, + (ArtifactFacet::Revision, Raw::Optional { present, text }) => { + record.revision = present.then(|| text.clone()) + } + (ArtifactFacet::Fragment, Raw::Optional { present, text }) => { + record.fragment = present.then(|| text.clone()) + } + _ => return Err("Artifact draft type disagrees with its selected metadata facet.".into()), + }; + Ok(()) +} +impl Editor { + pub fn pair(&self) -> KeyPair { + KeyPair { + oa: self.oa.as_ref().map(|c| c.id), + sa: self.sa.as_ref().map(|c| c.id), + } + } + pub fn select_pair(&mut self, p: &Project, facet: &FacetSelector) -> Result<(), String> { + let (keys, _) = parts(facet).ok_or("Choose an artifact address.")?; + self.oa = keys.oa.map(|id| self.choice(p, id)); + self.sa = keys.sa.map(|id| self.choice(p, id)); + Ok(()) + } + pub fn choice(&self, p: &Project, id: Id) -> Choice { + if p.contains(id) { + return view::choice(p, id); + } + self.seeds.get(&id).map_or_else( + || view::choice(p, id), + |a| Choice { + id, + label: format!("{} · reserved @{}", a.name, a.alias), + }, + ) + } + pub fn choices(&self, p: &Project, trace: &Trace, layer: Layer) -> Vec { + let owner = if layer == Layer::Oa { + trace.oa + } else { + trace.sa + }; + let selected = key(&self.pair(), layer); + let mut ids: BTreeSet<_> = p + .artifact_links + .values() + .filter(|a| p.layer_of(a.owner).is_none_or(|actual| actual == layer)) + .map(|a| a.id) + .chain( + self.seeds + .values() + .filter(|a| a.owner == owner) + .map(|a| a.id), + ) + .collect(); + if let Some(id) = selected { + ids.insert(id); + } + if let Some(base) = trace.baseline.as_ref().and_then(|b| b.semantic.as_ref()) { + for fact in &base.facets { + if let Some((keys, _)) = parts(&fact.selector()) + && let Some(id) = key(keys, layer) + { + ids.insert(id); + } + } + } + let search = self.search.to_lowercase(); + let mut choices: Vec<_> = ids + .into_iter() + .map(|id| { + let mut choice = self.choice(p, id); + if let Some(artifact) = p.artifact_links.get(&id) { + choice.label.push_str(&format!( + " · owner: {}{}", + p.named(artifact.owner), + if artifact.owner == owner { + "" + } else { + " · not attached to reviewed owner" + } + )); + } + choice + }) + .filter(|c| Some(c.id) == selected || c.label.to_lowercase().contains(&search)) + .collect(); + choices.sort_by_cached_key(|c| c.label.to_lowercase()); + choices + } + pub fn ensure( + &mut self, + p: &Project, + facet: &FacetSelector, + layer: Layer, + ) -> Result<(), String> { + let (keys, which) = parts(facet).ok_or("Choose artifact metadata.")?; + let id = key(keys, layer).ok_or("Choose a destination artifact identity.")?; + let record = p + .artifact_links + .get(&id) + .or_else(|| self.seeds.get(&id)) + .ok_or("Reserve a destination counterpart before reviewing its metadata.")?; + let value = raw(record, which).ok_or( + "Presence uses explicit Keep or Take; there is no deletion tombstone candidate.", + )?; + self.drafts.entry(facet.clone()).or_insert(value); + if which == ArtifactFacet::Description + && let Some(Raw::Text(text)) = self.drafts.get(facet) + { + self.descriptions + .entry(facet.clone()) + .or_insert_with(|| text_editor::Content::with_text(text)); + } + Ok(()) + } + pub fn update( + &mut self, + p: &Project, + trace: &Trace, + direction: TransitionDirection, + event: Event, + ) -> Result>, String> { + match event { + Event::Pick(layer, choice) => { + if layer == Layer::Oa { + self.oa = choice + } else { + self.sa = choice + } + } + Event::Search(search) => self.search = search, + Event::Stage => { + let keys = self.pair(); + if keys.oa.is_none() && keys.sa.is_none() { + return Err("Choose at least one exact artifact identity.".into()); + } + for layer in [Layer::Oa, Layer::Sa] { + if key(&keys, layer) + .and_then(|id| p.layer_of(id)) + .is_some_and(|actual| actual != layer) + { + return Err( + "Artifact pair identity belongs to the other perspective.".into() + ); + } + } + capture_facet( + p, + trace.oa, + trace.sa, + &selector(&keys, ArtifactFacet::Presence), + )?; + return Ok(Some( + std::iter::once(ArtifactFacet::Presence) + .chain(METADATA) + .map(|facet| selector(&keys, facet)) + .filter(|facet| capture_facet(p, trace.oa, trace.sa, facet).is_ok()) + .collect(), + )); + } + Event::Reserve => { + let source = key(&self.pair(), direction.source_layer()) + .and_then(|id| p.artifact_links.get(&id)) + .ok_or( + "Choose an existing source artifact before reserving its counterpart.", + )?; + let source_owner = if direction.source_layer() == Layer::Oa { + trace.oa + } else { + trace.sa + }; + if source.owner != source_owner { + return Err( + "The source artifact is not attached to this reviewed owner.".into(), + ); + } + let mut seed = source.clone(); + seed.id = Id::new_v4(); + seed.alias = format!("artifact_{}", seed.id.simple()); + seed.owner = if direction.target_layer() == Layer::Oa { + trace.oa + } else { + trace.sa + }; + let choice = Choice { + id: seed.id, + label: format!("{} · reserved @{}", seed.name, seed.alias), + }; + self.seeds.insert(seed.id, seed); + if direction.target_layer() == Layer::Oa { + self.oa = Some(choice) + } else { + self.sa = Some(choice) + }; + } + Event::Input(facet, value) => { + let (_, which) = parts(&facet).ok_or("Choose artifact metadata.")?; + if !compatible(which, &value) || !self.drafts.contains_key(&facet) { + return Err( + "Choose Set final value for this exact artifact metadata first.".into(), + ); + } + if which == ArtifactFacet::Description + && let Raw::Text(text) = &value + { + self.descriptions + .insert(facet.clone(), text_editor::Content::with_text(text)); + } + self.drafts.insert(facet, value); + } + Event::Description(facet, action) => { + let editor = self + .descriptions + .get_mut(&facet) + .ok_or("Choose the artifact description draft first.")?; + editor.perform(action); + self.drafts.insert(facet, Raw::Text(editor.text())); + } + } + Ok(None) + } + pub fn editable( + &self, + p: &Project, + trace: &Trace, + facet: &FacetSelector, + layer: Layer, + ) -> bool { + let Some((keys, which)) = parts(facet) else { + return false; + }; + key(keys, layer).is_some() + && capture_facet(p, trace.oa, trace.sa, facet).is_ok() + && (which == ArtifactFacet::Presence + || key(keys, layer).is_some_and(|id| { + p.artifact_links.contains_key(&id) || self.seeds.contains_key(&id) + })) + } + pub fn propose( + &self, + p: &Project, + trace: &Trace, + direction: TransitionDirection, + references: &BTreeMap, + decisions: &BTreeMap, + ) -> Result { + let mut result = Proposal { + slots: vec![], + creations: vec![], + candidates: BTreeSet::new(), + }; + let mut pairs: BTreeMap> = BTreeMap::new(); + for (facet, decision) in decisions { + if let Some((keys, _)) = parts(facet) { + let id = key(keys, direction.target_layer()) + .ok_or("Choose a destination artifact identity.")?; + pairs.entry(id).or_default().push((facet, *decision)); + } + } + for (id, selected) in pairs { + let original = p.artifact_links.get(&id); + let mut record = original.cloned(); + let mut deletion_origin = None; + let owner = if direction.target_layer() == Layer::Oa { + trace.oa + } else { + trace.sa + }; + for (facet, decision) in &selected { + let (keys, which) = parts(facet).expect("Artifact"); + if which != ArtifactFacet::Presence { + continue; + } + if *decision == SemanticDecision::Candidate { + return Err("Artifact presence uses Keep or Take; deletion requires explicit source-owner absence.".into()); + } + if *decision == SemanticDecision::TakeSource + && key(keys, direction.source_layer()).is_some() + { + let fact = capture_facet(p, trace.oa, trace.sa, facet)?; + let present = scalar::value(&fact, direction.source_layer()) + .and_then(|value| value.as_bool()) + .ok_or("Source artifact attachment unavailable.")?; + if !present { + record = None; + deletion_origin = Some((*facet).clone()); + } else if record.is_none() { + record=Some(self.seeds.get(&id).cloned().ok_or("Reserve a complete destination artifact counterpart before taking its presence.")?); + } else if record.as_ref().is_some_and(|a| a.owner != owner) { + return Err("Take presence cannot move an artifact from another owner; review its Owner from the initially attached trace.".into()); + } + } + } + for (facet, decision) in &selected { + let (keys, which) = parts(facet).expect("Artifact"); + if which == ArtifactFacet::Presence { + continue; + } + for (layer, side_owner) in [(Layer::Oa, trace.oa), (Layer::Sa, trace.sa)] { + if key(keys, layer) + .and_then(|id| p.artifact_links.get(&id)) + .is_some_and(|a| a.owner != side_owner) + { + return Err("Artifact metadata must be reviewed from its initially attached owner trace.".into()); + } + } + if *decision == SemanticDecision::KeepTarget { + continue; + } + let value = if *decision == SemanticDecision::Candidate { + self.drafts + .get(*facet) + .cloned() + .ok_or("Choose an explicit final metadata value.")? + } else { + if key(keys, direction.source_layer()).is_none() { + continue; + } + let source=key(keys,direction.source_layer()).and_then(|id|p.artifact_links.get(&id)).ok_or("Source artifact metadata is absent; keep or set the target metadata explicitly.")?; + let mut value = raw(source, which).expect("Metadata"); + if let Raw::Owner(ref mut owner) = value { + let from = source.owner; + let target = if direction == TransitionDirection::OaToSa { + references.get(&from).copied() + } else { + let ids: Vec<_> = references + .iter() + .filter(|(_, sa)| **sa == from) + .map(|(oa, _)| *oa) + .collect(); + (ids.len() == 1).then(|| ids[0]) + } + .ok_or("Map the source artifact owner explicitly.")?; + *owner = target.to_string(); + } + value + }; + write( + record + .as_mut() + .ok_or("This metadata decision conflicts with absent artifact presence.")?, + which, + &value, + )?; + if *decision == SemanticDecision::Candidate { + result.candidates.insert(id); + } + } + for (facet, decision) in &selected { + let (_, which) = parts(facet).expect("Artifact"); + result.slots.push(( + slot(id, which), + observation( + if *decision == SemanticDecision::KeepTarget { + original + } else { + record.as_ref() + }, + which, + ), + (*facet).clone(), + )); + } + if original.is_some() && record.is_none() { + for which in METADATA { + result.slots.push(( + slot(id, which), + observation(None, which), + deletion_origin + .clone() + .expect("Absent record has an explicit Presence decision"), + )); + } + } + if original.is_none() + && let Some(record) = record + { + result.candidates.insert(id); + result.creations.push(record); + } + } + Ok(result) + } + pub fn recovery_snapshot(&self) -> Saved { + Saved { + oa: self.oa.as_ref().map(|c| c.id), + sa: self.sa.as_ref().map(|c| c.id), + seeds: self.seeds.values().cloned().collect(), + drafts: self + .drafts + .iter() + .map(|(facet, value)| (facet.clone(), value.clone())) + .collect(), + } + } + pub fn from_recovery( + p: &Project, + trace: &Trace, + direction: TransitionDirection, + saved: Saved, + ) -> Result { + if saved.seeds.len().saturating_add(saved.drafts.len()) + > crate::source_workspace::LIMITS.model.max_records + { + return Err("Artifact recovery exceeds supported limits.".into()); + } + let mut editor = Self::default(); + let owner = if direction.target_layer() == Layer::Oa { + trace.oa + } else { + trace.sa + }; + let mut aliases = BTreeSet::new(); + for seed in saved.seeds { + if p.contains(seed.id) + || seed.owner != owner + || seed.alias != format!("artifact_{}", seed.id.simple()) + || !aliases.insert(seed.alias.clone()) + || editor.seeds.insert(seed.id, seed).is_some() + { + return Err("Recovered reserved artifact identity, owner or alias is invalid or duplicated.".into()); + } + } + for (facet, value) in saved.drafts { + let (keys, which) = + parts(&facet).ok_or("Recovered artifact draft has a different facet family.")?; + if !compatible(which, &value) { + return Err("Recovered artifact draft type disagrees with its facet.".into()); + } + for layer in [Layer::Oa, Layer::Sa] { + if key(keys, layer) + .and_then(|id| p.layer_of(id)) + .is_some_and(|actual| actual != layer) + { + return Err("Recovered artifact pair has a wrong-layer identity.".into()); + } + } + let id = key(keys, direction.target_layer()) + .ok_or("Recovered artifact draft has no destination identity.")?; + if !p.artifact_links.contains_key(&id) && !editor.seeds.contains_key(&id) { + return Err( + "Recovered artifact draft has no existing or reserved destination record." + .into(), + ); + } + if let (ArtifactFacet::Description, Raw::Text(text)) = (which, &value) { + editor + .descriptions + .insert(facet.clone(), text_editor::Content::with_text(text)); + } + if editor.drafts.insert(facet, value).is_some() { + return Err("Recovered artifact draft address is duplicated.".into()); + } + } + editor.oa = saved.oa.map(|id| editor.choice(p, id)); + editor.sa = saved.sa.map(|id| editor.choice(p, id)); + Ok(editor) + } +} diff --git a/crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs b/crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs new file mode 100644 index 00000000..b131de38 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs @@ -0,0 +1,328 @@ +//! Independent admission falsifiers: corrupt stored JSON, never private Saved structs. +use super::*; +use serde_json::json; + +struct Checkpoint { + project: Project, + state: State, + target_owner: Id, + source_link: Id, + keys: KeyPair, +} + +fn send(state: &mut State, project: &Project, event: RowEvent) { + let addressed = state.address_event(event); + state.update(project, addressed); +} + +fn facet(keys: &KeyPair, which: ArtifactFacet) -> FacetSelector { + FacetSelector::Artifact { + keys: keys.clone(), + facet: which, + } +} + +fn checkpoint(reverse: bool) -> Checkpoint { + let mut project = Project::empty(); + let oa = project + .add_element(Kind::Activity, Layer::Oa, "Reviewed activity", None) + .unwrap(); + let sa = project + .add_element(Kind::Function, Layer::Sa, "Reviewed function", None) + .unwrap(); + let (source_owner, target_owner, source_layer) = if reverse { + (sa, oa, Layer::Sa) + } else { + (oa, sa, Layer::Oa) + }; + let source_link = Id::new_v4(); + project.artifact_links.insert( + source_link, + ArtifactLink { + id: source_link, + alias: "reviewed_requirement".into(), + name: "Interlock evidence".into(), + description: "First line\r\nSecond line".into(), + owner: source_owner, + relation: ArtifactRelation::Verifies, + uri: "requirements/interlock".into(), + revision: None, + fragment: Some(String::new()), + }, + ); + let trace = super::tests::add_trace(&mut project, oa, sa, "Evidence correspondence"); + let mut state = State::new(&project, trace).unwrap(); + if reverse { + state.update(&project, Event::Direction(TransitionDirection::SaToOa)); + } + send( + &mut state, + &project, + RowEvent::Artifact(artifacts::Event::Pick( + source_layer, + Some(view::choice(&project, source_link)), + )), + ); + send( + &mut state, + &project, + RowEvent::Artifact(artifacts::Event::Reserve), + ); + send( + &mut state, + &project, + RowEvent::Artifact(artifacts::Event::Stage), + ); + let keys = state.artifacts.pair(); + for which in [ + ArtifactFacet::Presence, + ArtifactFacet::Name, + ArtifactFacet::Description, + ArtifactFacet::Owner, + ArtifactFacet::Relation, + ArtifactFacet::Uri, + ArtifactFacet::Revision, + ArtifactFacet::Fragment, + ] { + let address = facet(&keys, which); + send(&mut state, &project, RowEvent::Select(address.clone())); + send( + &mut state, + &project, + RowEvent::Decide( + address, + Some(if which == ArtifactFacet::Presence { + SemanticDecision::TakeSource + } else { + SemanticDecision::Candidate + }), + ), + ); + } + assert!(state.can_apply(), "{:?}", state.error); + for (which, raw) in [ + ( + ArtifactFacet::Owner, + artifacts::Raw::Owner("unfinished owner -".into()), + ), + ( + ArtifactFacet::Revision, + artifacts::Raw::Optional { + present: false, + text: "retained revision\r\nnext line".into(), + }, + ), + ( + ArtifactFacet::Fragment, + artifacts::Raw::Optional { + present: true, + text: String::new(), + }, + ), + ] { + send( + &mut state, + &project, + RowEvent::Artifact(artifacts::Event::Input(facet(&keys, which), raw)), + ); + } + assert!(!state.can_apply()); + Checkpoint { + project, + state, + target_owner, + source_link, + keys, + } +} + +fn artifacts_json(saved: &mut Value) -> &mut Value { + assert_eq!(saved["version"], 2); + // Switching direction retains the earlier row. Corrupt the exact active + // row's authored payload, regardless of retained rows or their ordering. + let active = saved["active"].clone(); + assert!(!active.is_null()); + let rows = saved["rows"].as_array_mut().unwrap(); + assert_eq!( + rows.iter().filter(|row| row["address"] == active).count(), + 1 + ); + &mut rows + .iter_mut() + .find(|row| row["address"] == active) + .unwrap()["draft"]["artifacts"] +} + +fn owner_draft<'a>(saved: &'a mut Value, keys: &KeyPair) -> &'a mut Value { + let address = serde_json::to_value(facet(keys, ArtifactFacet::Owner)).unwrap(); + artifacts_json(saved)["drafts"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|entry| entry[0] == address) + .expect("authored Owner draft") +} + +fn refuses_without_changing_authored_or_accepted_state( + case: &str, + checkpoint: &mut Checkpoint, + forged: Value, +) { + let before = checkpoint.project.clone(); + let source = oa_core::language::format(&before); + let authored = checkpoint.state.recovery_snapshot().unwrap(); + assert!( + State::from_recovery(&checkpoint.project, forged).is_err(), + "accepted forged {case}" + ); + assert_eq!( + checkpoint.state.recovery_snapshot().unwrap(), + authored, + "{case}" + ); + assert!( + checkpoint.state.command(&checkpoint.project).is_err(), + "invalid raw draft became executable: {case}" + ); + assert_eq!(checkpoint.project, before, "{case}"); + assert_eq!( + oa_core::language::format(&checkpoint.project), + source, + "{case}" + ); +} + +#[test] +fn forged_artifact_reservations_cannot_duplicate_or_claim_an_existing_identity_or_another_owner() { + for reverse in [false, true] { + let mut c = checkpoint(reverse); + let saved = c.state.recovery_snapshot().unwrap(); + for case in [ + "duplicate seed", + "existing identity", + "wrong owner", + "wrong alias", + ] { + let mut forged = saved.clone(); + let seeds = artifacts_json(&mut forged)["seeds"].as_array_mut().unwrap(); + assert_eq!(seeds.len(), 1); + match case { + "duplicate seed" => seeds.push(seeds[0].clone()), + "existing identity" => { + seeds[0]["id"] = json!(c.source_link); + seeds[0]["alias"] = json!(format!("artifact_{}", c.source_link.simple())); + } + "wrong owner" => seeds[0]["owner"] = json!(c.project.id), + "wrong alias" => seeds[0]["alias"] = json!("forged_alias"), + _ => unreachable!(), + } + refuses_without_changing_authored_or_accepted_state(case, &mut c, forged); + } + } +} + +#[test] +fn forged_artifact_raw_variants_duplicate_addresses_and_unknown_fields_are_refused() { + for reverse in [false, true] { + let mut c = checkpoint(reverse); + let saved = c.state.recovery_snapshot().unwrap(); + for case in [ + "wrong raw variant", + "duplicate facet", + "unknown artifact field", + ] { + let mut forged = saved.clone(); + match case { + "wrong raw variant" => { + owner_draft(&mut forged, &c.keys)[1] = json!({"Text": "unfinished owner -"}) + } + "duplicate facet" => { + let duplicate = owner_draft(&mut forged, &c.keys).clone(); + artifacts_json(&mut forged)["drafts"] + .as_array_mut() + .unwrap() + .push(duplicate); + } + "unknown artifact field" => artifacts_json(&mut forged)["execute"] = json!(true), + _ => unreachable!(), + } + refuses_without_changing_authored_or_accepted_state(case, &mut c, forged); + } + } +} + +#[test] +fn forged_artifact_draft_cannot_target_an_unreserved_or_non_artifact_identity() { + for reverse in [false, true] { + let mut c = checkpoint(reverse); + let saved = c.state.recovery_snapshot().unwrap(); + for (case, destination) in [ + ("nonexistent destination", Id::new_v4()), + ("element destination", c.target_owner), + ] { + let mut forged = saved.clone(); + let mut keys = c.keys.clone(); + if reverse { + keys.oa = Some(destination); + } else { + keys.sa = Some(destination); + } + owner_draft(&mut forged, &c.keys)[0] = + serde_json::to_value(facet(&keys, ArtifactFacet::Owner)).unwrap(); + refuses_without_changing_authored_or_accepted_state(case, &mut c, forged); + } + } +} + +#[test] +fn legitimate_invalid_artifact_checkpoint_round_trips_and_prepares_only_after_explicit_repair() { + for reverse in [false, true] { + let mut c = checkpoint(reverse); + let accepted = c.project.clone(); + let accepted_source = oa_core::language::format(&accepted); + let saved = c.state.recovery_snapshot().unwrap(); + let mut restored = State::from_recovery(&c.project, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + assert_eq!( + owner_draft(&mut saved.clone(), &c.keys)[1], + json!({"Owner": "unfinished owner -"}) + ); + assert!(!restored.can_apply()); + assert!(restored.command(&c.project).is_err()); + let repair = RowEvent::Artifact(artifacts::Event::Input( + facet(&c.keys, ArtifactFacet::Owner), + artifacts::Raw::Owner(c.target_owner.to_string()), + )); + send(&mut restored, &c.project, repair.clone()); + send(&mut c.state, &c.project, repair); + let restored_command = restored.command(&c.project).unwrap(); + let uninterrupted_command = c.state.command(&c.project).unwrap(); + let prepared = prepare_command(&c.project, &restored_command) + .unwrap() + .project; + let uninterrupted = prepare_command(&c.project, &uninterrupted_command) + .unwrap() + .project; + assert_eq!(prepared, uninterrupted); + assert_eq!( + oa_core::language::format(&prepared), + oa_core::language::format(&uninterrupted) + ); + let destination = if reverse { + c.keys.oa.unwrap() + } else { + c.keys.sa.unwrap() + }; + assert_eq!(prepared.artifact_links[&destination].revision, None); + assert_eq!( + prepared.artifact_links[&destination].fragment, + Some(String::new()) + ); + assert_eq!(c.project, accepted); + assert_eq!(oa_core::language::format(&c.project), accepted_source); + assert!( + !c.project.contains(destination), + "preparation must not execute creation" + ); + } +} diff --git a/crates/oa-desktop/src/semantic_review/artifacts_tests.rs b/crates/oa-desktop/src/semantic_review/artifacts_tests.rs new file mode 100644 index 00000000..5ef32071 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/artifacts_tests.rs @@ -0,0 +1,634 @@ +//! Core lifecycle boundaries and graphical artifact review oracles. +use super::*; +struct Fixture { + p: Project, + owners: [Id; 2], + other: [Id; 2], + links: [Id; 2], + trace: Id, +} +fn fixture() -> Fixture { + let mut p = Project::empty(); + let mut owners = vec![]; + let mut other = vec![]; + let mut links = vec![]; + for layer in [Layer::Oa, Layer::Sa] { + let kind = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + let owner = p.add_element(kind, layer, "Reviewed owner", None).unwrap(); + let moved = p.add_element(kind, layer, "Other owner", None).unwrap(); + let id = Id::new_v4(); + p.artifact_links.insert( + id, + ArtifactLink { + id, + alias: format!("artifact_{}", id.simple()), + name: "Interlock requirement".into(), + description: "Exact\r\nengineering rationale".into(), + owner, + relation: ArtifactRelation::Satisfies, + uri: "requirements/interlock".into(), + revision: None, + fragment: Some(String::new()), + }, + ); + owners.push(owner); + other.push(moved); + links.push(id); + } + let trace = super::tests::add_trace(&mut p, owners[0], owners[1], "Requirement correspondence"); + Fixture { + p, + owners: [owners[0], owners[1]], + other: [other[0], other[1]], + links: [links[0], links[1]], + trace, + } +} +fn selector(keys: &KeyPair, facet: ArtifactFacet) -> FacetSelector { + FacetSelector::Artifact { + keys: keys.clone(), + facet, + } +} +fn request( + f: &Fixture, + reverse: bool, + keys: &KeyPair, + decisions: Vec<(ArtifactFacet, SemanticDecision)>, +) -> SemanticTransitionRequest { + let source = usize::from(reverse); + let target = 1 - source; + let mut transition = TransitionRequest::new( + if reverse { + TransitionDirection::SaToOa + } else { + TransitionDirection::OaToSa + }, + vec![f.owners[source]], + ); + transition.mappings.push(TransitionMapping { + source: f.owners[source], + target: Some(f.owners[target]), + target_kind: None, + }); + let mut r = SemanticTransitionRequest::new(transition); + r.traces.insert(f.owners[source], f.trace); + r.references.insert(f.owners[0], f.owners[1]); + if let Some((oa, sa)) = keys.oa.zip(keys.sa) { + r.references.insert(oa, sa); + } + r.reviews.push(SemanticReviewRequest { + source: f.owners[source], + resolutions: decisions + .into_iter() + .map(|(facet, decision)| FacetResolution { + facet: selector(keys, facet), + decision, + }) + .collect(), + reset: vec![], + }); + r +} +const METADATA: [ArtifactFacet; 7] = [ + ArtifactFacet::Name, + ArtifactFacet::Description, + ArtifactFacet::Owner, + ArtifactFacet::Relation, + ArtifactFacet::Uri, + ArtifactFacet::Revision, + ArtifactFacet::Fragment, +]; +#[test] +fn artifact_creation_and_identified_source_deletion_follow_core_metadata_and_history_boundary_both_directions() + { + for reverse in [false, true] { + let mut f = fixture(); + let source = usize::from(reverse); + let target = 1 - source; + f.p.artifact_links.remove(&f.links[target]); + let keys = KeyPair { + oa: Some(f.links[0]), + sa: Some(f.links[1]), + }; + let mut candidate = f.p.artifact_links[&f.links[source]].clone(); + candidate.id = f.links[target]; + candidate.alias = format!("artifact_{}", candidate.id.simple()); + candidate.owner = f.owners[target]; + let mut r = request( + &f, + reverse, + &keys, + vec![(ArtifactFacet::Presence, SemanticDecision::TakeSource)], + ); + r.candidates + .push(SemanticRecord::Artifact(candidate.clone())); + assert!( + !plan_semantic_transition(&f.p, r.clone()) + .unwrap() + .conflicts + .is_empty() + ); + r.reviews[0] + .resolutions + .extend(METADATA.map(|facet| FacetResolution { + facet: selector(&keys, facet), + decision: SemanticDecision::Candidate, + })); + let plan = plan_semantic_transition(&f.p, r).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + f.p = prepare_semantic_transition(&f.p, &plan).unwrap().project; + assert_eq!(f.p.artifact_links[&f.links[target]], candidate); + assert_eq!( + f.p.traces[&f.trace] + .baseline + .as_ref() + .unwrap() + .semantic + .as_ref() + .unwrap() + .facets + .len(), + 8 + ); + f.p = prepare_command(&f.p, &Command::Delete(f.links[source])) + .unwrap() + .project; + let deletion = request( + &f, + reverse, + &keys, + vec![(ArtifactFacet::Presence, SemanticDecision::TakeSource)], + ); + let plan = plan_semantic_transition(&f.p, deletion).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + assert_eq!(plan.reviews[0].retired.len(), 7); + let after = prepare_semantic_transition(&f.p, &plan).unwrap().project; + assert!(!after.artifact_links.contains_key(&f.links[target])); + } +} +#[test] +fn artifact_candidate_is_a_complete_record_not_a_tombstone_or_unattached_creation_shortcut() { + for reverse in [false, true] { + let mut f = fixture(); + let target = usize::from(!reverse); + let keys = KeyPair { + oa: Some(f.links[0]), + sa: Some(f.links[1]), + }; + let mut r = request( + &f, + reverse, + &keys, + vec![(ArtifactFacet::Presence, SemanticDecision::Candidate)], + ); + let mut moved = f.p.artifact_links[&f.links[target]].clone(); + moved.owner = f.other[target]; + r.candidates.push(SemanticRecord::Artifact(moved)); + assert!( + !plan_semantic_transition(&f.p, r) + .unwrap() + .conflicts + .is_empty(), + "A moved record cannot serve as a deletion tombstone" + ); + let mut candidate = f.p.artifact_links[&f.links[target]].clone(); + candidate.id = Id::new_v4(); + candidate.alias = format!("artifact_{}", candidate.id.simple()); + f.p.artifact_links.clear(); + let keys = if reverse { + KeyPair { + oa: Some(candidate.id), + sa: None, + } + } else { + KeyPair { + oa: None, + sa: Some(candidate.id), + } + }; + let mut decisions = vec![(ArtifactFacet::Presence, SemanticDecision::Candidate)]; + decisions.extend(METADATA.map(|facet| (facet, SemanticDecision::Candidate))); + let mut r = request(&f, reverse, &keys, decisions); + r.candidates.push(SemanticRecord::Artifact(candidate)); + assert!( + !plan_semantic_transition(&f.p, r) + .unwrap() + .conflicts + .is_empty(), + "Metadata has no applicable initial source or target record" + ); + } +} + +fn send(state: &mut State, p: &Project, event: RowEvent) { + let event = state.address_event(event); + state.update(p, event); +} +fn open(f: &Fixture, reverse: bool, keys: &KeyPair) -> State { + let mut state = State::new(&f.p, f.trace).unwrap(); + if reverse { + state.update(&f.p, Event::Direction(TransitionDirection::SaToOa)); + } + send( + &mut state, + &f.p, + RowEvent::Artifact(artifacts::Event::Pick( + Layer::Oa, + keys.oa.map(|id| view::choice(&f.p, id)), + )), + ); + send( + &mut state, + &f.p, + RowEvent::Artifact(artifacts::Event::Pick( + Layer::Sa, + keys.sa.map(|id| view::choice(&f.p, id)), + )), + ); + send( + &mut state, + &f.p, + RowEvent::Artifact(artifacts::Event::Stage), + ); + state +} +fn decide( + state: &mut State, + p: &Project, + keys: &KeyPair, + which: ArtifactFacet, + decision: SemanticDecision, +) { + let facet = selector(keys, which); + send(state, p, RowEvent::Select(facet.clone())); + send(state, p, RowEvent::Decide(facet, Some(decision))); +} +fn input( + state: &mut State, + p: &Project, + keys: &KeyPair, + which: ArtifactFacet, + value: artifacts::Raw, +) { + send( + state, + p, + RowEvent::Artifact(artifacts::Event::Input(selector(keys, which), value)), + ); +} +fn errors(state: &State) -> String { + format!( + "{:?} / {:?}", + state.error, + state.plan.as_ref().map(|p| &p.conflicts) + ) +} +#[test] +fn artifact_gui_reserves_recovers_and_creates_only_after_all_seven_metadata_reviews_both_directions() + { + for reverse in [false, true] { + let mut f = fixture(); + let target = usize::from(!reverse); + f.p.artifact_links.remove(&f.links[target]); + let keys = if reverse { + KeyPair { + oa: None, + sa: Some(f.links[1]), + } + } else { + KeyPair { + oa: Some(f.links[0]), + sa: None, + } + }; + let mut state = open(&f, reverse, &keys); + send( + &mut state, + &f.p, + RowEvent::Artifact(artifacts::Event::Reserve), + ); + send( + &mut state, + &f.p, + RowEvent::Artifact(artifacts::Event::Stage), + ); + let keys = state.artifacts.pair(); + let id = artifacts::key(&keys, state.direction.target_layer()).unwrap(); + assert!(!f.p.contains(id)); + let saved = state.recovery_snapshot().unwrap(); + let mut state = State::from_recovery(&f.p, saved.clone()).unwrap(); + assert_eq!(state.recovery_snapshot().unwrap(), saved); + assert_eq!(state.artifacts.pair(), keys); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Presence, + SemanticDecision::TakeSource, + ); + assert!(!state.can_apply()); + for facet in METADATA { + decide(&mut state, &f.p, &keys, facet, SemanticDecision::Candidate); + } + assert!(state.can_apply(), "{}", errors(&state)); + let mut direct = request( + &f, + reverse, + &keys, + vec![(ArtifactFacet::Presence, SemanticDecision::TakeSource)], + ); + direct.reviews[0] + .resolutions + .extend(METADATA.map(|facet| FacetResolution { + facet: selector(&keys, facet), + decision: SemanticDecision::Candidate, + })); + direct + .candidates + .push(SemanticRecord::Artifact(state.artifacts.seeds[&id].clone())); + let plan = plan_semantic_transition(&f.p, direct).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let expected = prepare_semantic_transition(&f.p, &plan).unwrap().project; + assert_eq!(state.proposed.as_ref(), Some(&expected)); + assert_eq!(expected.artifact_links[&id].revision, None); + assert_eq!(expected.artifact_links[&id].fragment, Some(String::new())); + } +} +#[test] +fn artifact_move_metadata_none_empty_and_invalid_raw_recovery_are_independent_both_directions() { + for reverse in [false, true] { + let f = fixture(); + let keys = KeyPair { + oa: Some(f.links[0]), + sa: Some(f.links[1]), + }; + let target = usize::from(!reverse); + let mut state = open(&f, reverse, &keys); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Owner, + SemanticDecision::Candidate, + ); + input( + &mut state, + &f.p, + &keys, + ArtifactFacet::Owner, + artifacts::Raw::Owner(f.other[target].to_string()), + ); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Revision, + SemanticDecision::Candidate, + ); + input( + &mut state, + &f.p, + &keys, + ArtifactFacet::Revision, + artifacts::Raw::Optional { + present: true, + text: String::new(), + }, + ); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Fragment, + SemanticDecision::Candidate, + ); + input( + &mut state, + &f.p, + &keys, + ArtifactFacet::Fragment, + artifacts::Raw::Optional { + present: false, + text: "retained fragment draft".into(), + }, + ); + assert!(state.can_apply(), "{}", errors(&state)); + let result = state.proposed.as_ref().unwrap(); + assert_eq!( + result.artifact_links[&f.links[target]].owner, + f.other[target] + ); + assert_eq!( + result.artifact_links[&f.links[target]].revision, + Some(String::new()) + ); + assert_eq!(result.artifact_links[&f.links[target]].fragment, None); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Presence, + SemanticDecision::KeepTarget, + ); + assert!(!state.can_apply()); + assert!(errors(&state).contains("disagree"), "{}", errors(&state)); + send( + &mut state, + &f.p, + RowEvent::Decide(selector(&keys, ArtifactFacet::Presence), None), + ); + assert!(state.can_apply(), "{}", errors(&state)); + input( + &mut state, + &f.p, + &keys, + ArtifactFacet::Owner, + artifacts::Raw::Owner("-".into()), + ); + assert!(!state.can_apply()); + let address = state.active_address.unwrap(); + state.update(&f.p, Event::Include(address, false)); + let saved = state.recovery_snapshot().unwrap(); + let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + assert_eq!( + restored.artifacts.drafts[&selector(&keys, ArtifactFacet::Owner)], + artifacts::Raw::Owner("-".into()) + ); + assert_eq!( + restored.artifacts.drafts[&selector(&keys, ArtifactFacet::Fragment)], + artifacts::Raw::Optional { + present: false, + text: "retained fragment draft".into() + } + ); + restored.update(&f.p, Event::Include(address, true)); + input( + &mut restored, + &f.p, + &keys, + ArtifactFacet::Owner, + artifacts::Raw::Owner(f.other[target].to_string()), + ); + assert!(restored.can_apply(), "{}", errors(&restored)); + assert_eq!(f.p.artifact_links[&f.links[target]].owner, f.owners[target]); + } +} +#[test] +fn artifact_moved_source_absence_deletes_but_destination_only_take_retains_both_directions() { + for reverse in [false, true] { + let mut f = fixture(); + let source = usize::from(reverse); + let target = 1 - source; + f.p.artifact_links.get_mut(&f.links[source]).unwrap().owner = f.other[source]; + // No selected pair or historical baseline can smuggle this option into + // the list: a moved-away existing link must be directly selectable. + let choices = artifacts::Editor::default().choices( + &f.p, + &f.p.traces[&f.trace], + if reverse { Layer::Sa } else { Layer::Oa }, + ); + let moved = choices + .iter() + .find(|choice| choice.id == f.links[source]) + .expect("Moved-away source is a real picker option"); + assert!(moved.label.contains("Other owner"), "{}", moved.label); + assert!( + moved.label.contains("not attached to reviewed owner"), + "{}", + moved.label + ); + assert!(!choices.iter().any(|choice| choice.id == f.links[target])); + let mut neutral = f.p.clone(); + neutral + .artifact_links + .get_mut(&f.links[source]) + .unwrap() + .owner = neutral.id; + let choices = artifacts::Editor::default().choices( + &neutral, + &neutral.traces[&f.trace], + if reverse { Layer::Sa } else { Layer::Oa }, + ); + assert!( + choices.iter().any(|choice| choice.id == f.links[source]), + "A link moved to a neutral project owner still has an explicit source-owner absence" + ); + + let mut keys = KeyPair { + oa: Some(f.links[0]), + sa: Some(f.links[1]), + }; + let mut state = open(&f, reverse, &keys); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Presence, + SemanticDecision::TakeSource, + ); + assert!(state.can_apply(), "{}", errors(&state)); + assert!( + !state + .proposed + .as_ref() + .unwrap() + .artifact_links + .contains_key(&f.links[target]) + ); + assert_eq!( + state.proposed.as_ref().unwrap().artifact_links[&f.links[source]], + f.p.artifact_links[&f.links[source]] + ); + if reverse { + keys.sa = None + } else { + keys.oa = None + }; + let mut state = open(&f, reverse, &keys); + decide( + &mut state, + &f.p, + &keys, + ArtifactFacet::Presence, + SemanticDecision::TakeSource, + ); + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + state.proposed.as_ref().unwrap().artifact_links[&f.links[target]], + f.p.artifact_links[&f.links[target]] + ); + } +} +#[test] +fn artifact_metadata_apply_and_exact_history_reset_are_one_source_preserving_undo_redo() { + use crate::app::App; + let f = fixture(); + let keys = KeyPair { + oa: Some(f.links[0]), + sa: Some(f.links[1]), + }; + let mut app = App::with_project(f.p.clone()); + let mut bundle = app.document.bundle().clone(); + bundle + .files + .get_mut(&bundle.entry.clone()) + .unwrap() + .insert_str(0, "// Artifact rationale stays verbatim\r\n"); + app.document.apply_bundle(bundle).unwrap(); + let before = app.document.snapshot(); + super::tests::start(&mut app, f.trace); + super::tests::send( + &mut app, + RowEvent::EditPair(selector(&keys, ArtifactFacet::Name)), + ); + super::tests::send(&mut app, RowEvent::Artifact(artifacts::Event::Stage)); + super::tests::decide( + &mut app, + selector(&keys, ArtifactFacet::Name), + SemanticDecision::Candidate, + ); + super::tests::send( + &mut app, + RowEvent::Artifact(artifacts::Event::Input( + selector(&keys, ArtifactFacet::Name), + artifacts::Raw::Text("Reviewed interlock requirement".into()), + )), + ); + assert!( + super::tests::state(&app).can_apply(), + "{}", + errors(super::tests::state(&app)) + ); + let expected = super::tests::state(&app).proposed.clone().unwrap(); + super::tests::apply(&mut app); + assert_eq!(app.project(), &expected); + let after = app.document.snapshot(); + assert!( + after.bundle.files[&after.bundle.entry] + .starts_with("// Artifact rationale stays verbatim\r\n") + ); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &before.bundle); + let _ = app.update(Message::Redo); + assert_eq!(app.document.bundle(), &after.bundle); + super::tests::start(&mut app, f.trace); + super::tests::send( + &mut app, + RowEvent::Reset(selector(&keys, ArtifactFacet::Name), true), + ); + assert!( + super::tests::state(&app).can_apply(), + "{}", + errors(super::tests::state(&app)) + ); + super::tests::apply(&mut app); + assert_eq!(app.project().artifact_links, expected.artifact_links); + let _ = app.update(Message::Undo); + assert_eq!(app.document.bundle(), &after.bundle); +} diff --git a/crates/oa-desktop/src/semantic_review/artifacts_view.rs b/crates/oa-desktop/src/semantic_review/artifacts_view.rs new file mode 100644 index 00000000..ea186228 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/artifacts_view.rs @@ -0,0 +1,147 @@ +use super::artifacts::{Editor, Event, Raw}; +use super::*; +use iced::{ + Element as View, Fill, + widget::{button, checkbox, column, pick_list, row, text, text_editor, text_input}, +}; +pub(super) fn pairs<'a>(editor: &'a Editor, p: &'a Project, trace: &'a Trace) -> View<'a, Event> { + let mut content=column![text("Artifact links and metadata").size(16),text("Pair links attached to these reviewed owners. Presence is attachment to the owner; taking identified source absence deletes the destination link, while a destination-only link is retained.").size(12),text_input("Filter artifact identities",&editor.search).on_input(Event::Search).padding(7)].spacing(9); + for (layer, label, selected) in [ + (Layer::Oa, "Operational", editor.oa.clone()), + (Layer::Sa, "System", editor.sa.clone()), + ] { + content = content.push(text(label).size(12)).push( + row![ + pick_list(editor.choices(p, trace, layer), selected, move |v| { + Event::Pick(layer, Some(v)) + }) + .placeholder("Choose exact artifact identity") + .width(Fill) + .padding(7), + button("No identity") + .padding(7) + .on_press(Event::Pick(layer, None)) + ] + .spacing(7), + ); + } + content.push(row![button("Reserve a new destination counterpart").padding(7).on_press(Event::Reserve),button("Review this exact artifact pair").padding(7).on_press(Event::Stage)].spacing(7).wrap()).push(text("Reservation retains a fresh identity and seeds metadata from the source. It creates nothing until Presence and all seven metadata facets are explicitly reviewed and the combined Apply succeeds. New unrelated links use ordinary artifact creation.").size(11)).padding(iced::Padding::default().right(18)).into() +} +pub(super) fn candidate<'a>( + editor: &'a Editor, + p: &'a Project, + facet: &'a FacetSelector, +) -> Option> { + let value = editor.drafts.get(facet)?; + let (_, which) = artifacts::parts(facet)?; + let mut content = + column![text(format!("Final artifact {}", artifacts::field(which)?)).size(12)].spacing(7); + match value { + Raw::Text(_) if which == ArtifactFacet::Description => { + let draft = editor.descriptions.get(facet)?; + let facet = facet.clone(); + content = content.push( + text_editor(draft) + .height(110) + .on_action(move |action| Event::Description(facet.clone(), action)), + ); + } + Raw::Text(value) => { + let facet = facet.clone(); + content = content.push( + text_input("Exact text", value) + .on_input(move |value| Event::Input(facet.clone(), Raw::Text(value))) + .padding(7), + ); + } + Raw::Owner(value) => { + let mut choices: Vec<_> = p + .identities() + .into_iter() + .map(|(id, alias)| Choice { + id, + label: format!("{} · @{alias}", p.named(id)), + }) + .collect(); + choices.sort_by_cached_key(|c| c.label.to_lowercase()); + let selected = value.parse::().ok().map(|id| view::choice(p, id)); + let raw_facet = facet.clone(); + let pick_facet = facet.clone(); + content = content + .push( + pick_list(choices, selected, move |v: Choice| { + Event::Input(pick_facet.clone(), Raw::Owner(v.id.to_string())) + }) + .placeholder("Explicit final owner") + .width(Fill) + .padding(7), + ) + .push( + text_input("Owner identity (retains rejected input)", value) + .on_input(move |v| Event::Input(raw_facet.clone(), Raw::Owner(v))) + .padding(7), + ); + } + Raw::Relation(value) => { + let facet = facet.clone(); + let variants = [ + ArtifactRelation::References, + ArtifactRelation::Satisfies, + ArtifactRelation::Verifies, + ArtifactRelation::Implements, + ArtifactRelation::DerivedFrom, + ]; + let choices: Vec<_> = variants.into_iter().map(RelationChoice).collect(); + content = content.push( + pick_list(choices, Some(RelationChoice(*value)), move |v| { + Event::Input(facet.clone(), Raw::Relation(v.0)) + }) + .padding(7), + ); + } + Raw::Optional { + present, + text: value, + } => { + let toggle_facet = facet.clone(); + let retained = value.clone(); + let input_facet = facet.clone(); + let enabled = *present; + content = content + .push( + checkbox(*present) + .label("Present (checked with empty text means an explicit empty value)") + .on_toggle(move |present| { + Event::Input( + toggle_facet.clone(), + Raw::Optional { + present, + text: retained.clone(), + }, + ) + }), + ) + .push( + text_input("Retained optional text", value) + .on_input(move |text| { + Event::Input( + input_facet.clone(), + Raw::Optional { + present: enabled, + text, + }, + ) + }) + .padding(7), + ); + } + } + Some(content.into()) +} +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct RelationChoice(ArtifactRelation); +impl std::fmt::Display for RelationChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/oa-desktop/src/semantic_review/attachments.rs b/crates/oa-desktop/src/semantic_review/attachments.rs new file mode 100644 index 00000000..119d81cc --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/attachments.rs @@ -0,0 +1,456 @@ +//! Existing-record child/incident editing with independent exact-field drafts. +//! Every proposal names its actual Parent or relationship endpoint slot. +use super::*; +use crate::forms::RecoveryDraft; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum Family { + #[default] + Child, + Source, + Target, +} +impl std::fmt::Display for Family { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Child => "Owned children", + Self::Source => "Relationships at source", + Self::Target => "Relationships at target", + }) + } +} +#[derive(Debug, Clone)] +pub enum Event { + Family(Family), + Pick(Layer, Option), + Search(String), + Stage, +} +#[derive(Default)] +struct PairDraft { + oa: Option, + sa: Option, +} +#[derive(Default)] +pub struct Editor { + pub family: Family, + pub search: String, + pairs: BTreeMap, + pub drafts: BTreeMap, +} +#[derive(Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Saved { + family: Family, + pairs: Vec<(Family, Option, Option)>, + drafts: Vec<(FacetSelector, RecoveryDraft)>, +} +pub struct Proposal { + pub slot: (Id, FacetSelector), + pub path: String, + pub record: Value, +} +pub(super) fn family(facet: &FacetSelector) -> Option { + Some(match facet { + FacetSelector::Child(_) => Family::Child, + FacetSelector::Incident { + end: IncidentEnd::Source, + .. + } => Family::Source, + FacetSelector::Incident { + end: IncidentEnd::Target, + .. + } => Family::Target, + _ => return None, + }) +} +pub(super) fn supported(facet: &FacetSelector) -> bool { + family(facet).is_some() +} +fn keys(facet: &FacetSelector) -> Option<&KeyPair> { + match facet { + FacetSelector::Child(keys) | FacetSelector::Incident { keys, .. } => Some(keys), + _ => None, + } +} +pub(super) fn key(facet: &FacetSelector, layer: Layer) -> Option { + let keys = keys(facet)?; + if layer == Layer::Oa { keys.oa } else { keys.sa } +} +pub(super) fn explicit_pair(facet: &FacetSelector) -> Option<(Id, Id)> { + let keys = keys(facet)?; + keys.oa.zip(keys.sa) +} +pub(super) fn actual_slot(facet: &FacetSelector, layer: Layer) -> Option<(Id, FacetSelector)> { + let id = key(facet, layer)?; + Some(( + id, + match family(facet)? { + Family::Child => FacetSelector::Header(HeaderFacet::Parent), + Family::Source => FacetSelector::Relation(RelationFacet::Source), + Family::Target => FacetSelector::Relation(RelationFacet::Target), + }, + )) +} +pub(super) fn path(project: &Project, facet: &FacetSelector, layer: Layer) -> Option { + let (id, slot) = actual_slot(facet, layer)?; + if project.layer_of(id) != Some(layer) { + return None; + } + // Child keys cannot accidentally name a relationship (and vice versa). + match family(facet)? { + Family::Child if !project.elements.contains_key(&id) => return None, + Family::Source | Family::Target if !project.relations.contains_key(&id) => return None, + _ => {} + } + scalar::binding(project, id, &slot) +} +pub(super) fn label(facet: &FacetSelector) -> &'static str { + match family(facet) { + Some(Family::Child) => "Final parent of the destination child", + Some(Family::Source) => "Final source of the destination relationship", + Some(Family::Target) => "Final target of the destination relationship", + None => "Unavailable attachment", + } +} +impl Editor { + pub fn selection(&self, layer: Layer) -> Option<&Choice> { + let pair = self.pairs.get(&self.family)?; + if layer == Layer::Oa { + pair.oa.as_ref() + } else { + pair.sa.as_ref() + } + } + pub fn update( + &mut self, + project: &Project, + trace: &Trace, + event: Event, + ) -> Result, String> { + match event { + Event::Family(family) => self.family = family, + Event::Search(search) => self.search = search, + Event::Pick(layer, choice) => { + let pair = self.pairs.entry(self.family).or_default(); + if layer == Layer::Oa { + pair.oa = choice; + } else { + pair.sa = choice; + } + } + Event::Stage => { + let keys = KeyPair { + oa: self.selection(Layer::Oa).map(|c| c.id), + sa: self.selection(Layer::Sa).map(|c| c.id), + }; + let facet = match self.family { + Family::Child => FacetSelector::Child(keys), + Family::Source => FacetSelector::Incident { + keys, + end: IncidentEnd::Source, + }, + Family::Target => FacetSelector::Incident { + keys, + end: IncidentEnd::Target, + }, + }; + for (id, expected) in [ + (key(&facet, Layer::Oa), Layer::Oa), + (key(&facet, Layer::Sa), Layer::Sa), + ] { + if id + .and_then(|id| project.layer_of(id)) + .is_some_and(|actual| actual != expected) + { + return Err( + "Attachment identities must match their Operational or System side." + .into(), + ); + } + } + capture_facet(project, trace.oa, trace.sa, &facet)?; + return Ok(Some(facet)); + } + } + Ok(None) + } + pub fn select_pair(&mut self, project: &Project, facet: &FacetSelector) -> Result<(), String> { + self.family = family(facet).ok_or("Choose a child or incident relationship address.")?; + let keys = keys(facet).expect("Closed family"); + self.pairs.insert( + self.family, + PairDraft { + oa: keys.oa.map(|id| view::choice(project, id)), + sa: keys.sa.map(|id| view::choice(project, id)), + }, + ); + Ok(()) + } + pub fn ensure( + &mut self, + project: &Project, + facet: &FacetSelector, + layer: Layer, + ) -> Result<(), String> { + let id = key(facet, layer).ok_or("Choose an existing destination attachment identity.")?; + path(project, facet, layer).ok_or("The destination attachment record is absent or has a different kind. Creation needs an explicit transition mapping.")?; + if !self.drafts.contains_key(facet) { + self.drafts.insert( + facet.clone(), + Draft::from_project(project, id) + .ok_or("Destination attachment record is absent.")?, + ); + } + Ok(()) + } + pub fn choices(&self, project: &Project, trace: &Trace, layer: Layer) -> Vec { + let owner = if layer == Layer::Oa { + trace.oa + } else { + trace.sa + }; + let owner_kind = project.elements.get(&owner).map(Element::kind); + let mut ids: BTreeSet<_> = match self.family { + Family::Child => project + .elements + .values() + .filter(|e| { + e.layer() == layer + && e.id != owner + && (e.parent == Some(owner) + || owner_kind.is_some_and(|kind| { + oa_core::validation::parent_permits(kind, e.kind()) + })) + }) + .map(|e| e.id) + .collect(), + Family::Source | Family::Target => project + .relations + .values() + .filter(|relation| { + if project.layer_of(relation.id) != Some(layer) { + return false; + } + let (a, b) = relation.body.endpoints(); + let (current, opposite) = if self.family == Family::Source { + (a, b) + } else { + (b, a) + }; + if current == owner { + return true; + } + match (owner_kind, project.elements.get(&opposite)) { + (Some(owner_kind), Some(opposite)) => { + if self.family == Family::Source { + relation.kind().permits(owner_kind, opposite.kind()) + } else { + relation.kind().permits(opposite.kind(), owner_kind) + } + } + // Rare relationship-to-relationship endpoints use the ordinary + // closed property picker rather than assuming element kinds. + _ => { + let Some(draft) = Draft::from_project(project, relation.id) else { + return false; + }; + let facet = FacetSelector::Relation(if self.family == Family::Source { + RelationFacet::Source + } else { + RelationFacet::Target + }); + let Some(path) = scalar::binding(project, relation.id, &facet) else { + return false; + }; + crate::forms::reference_choices(&draft, project, &path, Id::nil()) + .iter() + .any(|choice| choice.id == owner) + } + } + }) + .map(|relation| relation.id) + .collect(), + }; + if let Some(selected) = self.selection(layer) { + ids.insert(selected.id); + } + let search = self.search.to_lowercase(); + let mut choices: Vec<_> = ids + .into_iter() + .map(|id| view::choice(project, id)) + .filter(|choice| { + search.is_empty() + || choice.label.to_lowercase().contains(&search) + || self + .selection(layer) + .is_some_and(|selected| selected.id == choice.id) + }) + .collect(); + choices.sort_by_cached_key(|choice| choice.label.to_lowercase()); + choices + } + pub fn propose( + &self, + project: &Project, + trace: &Trace, + direction: TransitionDirection, + facet: &FacetSelector, + decision: SemanticDecision, + form_context: bool, + ) -> Result { + let layer = direction.target_layer(); + let slot = actual_slot(facet, layer) + .ok_or("Choose the destination attachment identity explicitly.")?; + let path = path(project, facet, layer).ok_or( + "Only existing destination child and relationship records are supported here.", + )?; + let mut record = Draft::from_project(project, slot.0) + .ok_or("The attached record is absent.")? + .value; + let owner = if layer == Layer::Oa { + trace.oa + } else { + trace.sa + }; + let value = match decision { + SemanticDecision::KeepTarget => record.pointer(&path).expect("Closed path").clone(), + SemanticDecision::Candidate => { + let draft = self + .drafts + .get(facet) + .ok_or("Set a final attachment value first.")?; + let supplied = match draft.materialize_scopes(std::slice::from_ref(&path)) { + Ok(value) => value, + Err(_) if form_context => draft.value.clone(), + Err(error) => return Err(error), + }; + supplied + .pointer(&path) + .cloned() + .ok_or("The attachment's exact field is unavailable.")? + } + SemanticDecision::TakeSource if key(facet, direction.source_layer()).is_none() => { + record.pointer(&path).expect("Closed path").clone() + } + SemanticDecision::TakeSource => { + let source = capture_facet(project, trace.oa, trace.sa, facet)?; + let present = scalar::value(&source, direction.source_layer()).and_then(|value| value.as_bool()).ok_or("Source attachment does not apply; review an existing owner correspondence.")?; + if present { + serde_json::to_value(owner).expect("Id") + } else if family(facet) == Some(Family::Child) { + if record.pointer(&path) == Some(&serde_json::to_value(owner).expect("Id")) { + Value::Null + } else { + record.pointer(&path).expect("Closed path").clone() + } + } else { + return Err("Taking an absent relationship endpoint is incomplete. Choose Set final value and an explicit replacement endpoint.".into()); + } + } + }; + *record.pointer_mut(&path).expect("Closed path") = value; + Ok(Proposal { slot, path, record }) + } + pub fn recovery_snapshot(&self) -> Saved { + Saved { + family: self.family, + pairs: self + .pairs + .iter() + .map(|(family, pair)| { + ( + *family, + pair.oa.as_ref().map(|c| c.id), + pair.sa.as_ref().map(|c| c.id), + ) + }) + .collect(), + drafts: self + .drafts + .iter() + .map(|(facet, draft)| (facet.clone(), draft.recovery_snapshot())) + .collect(), + } + } + pub fn from_recovery(project: &Project, layer: Layer, saved: Saved) -> Result { + if saved.drafts.len() > crate::source_workspace::LIMITS.model.max_records + || saved.pairs.len() > 3 + { + return Err("Recovered attachment drafts exceed the supported limits.".into()); + } + let mut editor = Self { + family: saved.family, + ..Self::default() + }; + for (family, oa, sa) in saved.pairs { + for (id, layer) in [(oa, Layer::Oa), (sa, Layer::Sa)] { + if id + .and_then(|id| project.layer_of(id)) + .is_some_and(|actual| actual != layer) + { + return Err("Recovered attachment picker has a wrong-layer identity.".into()); + } + } + if editor + .pairs + .insert( + family, + PairDraft { + oa: oa.map(|id| view::choice(project, id)), + sa: sa.map(|id| view::choice(project, id)), + }, + ) + .is_some() + { + return Err("Recovered attachment picker family is duplicated.".into()); + } + } + for (facet, saved) in saved.drafts { + for expected in [Layer::Oa, Layer::Sa] { + if key(&facet, expected) + .and_then(|id| project.layer_of(id)) + .is_some_and(|actual| actual != expected) + { + return Err("Recovered attachment address has a wrong-layer identity.".into()); + } + } + let (id, _) = actual_slot(&facet, layer) + .ok_or("Recovered attachment has no destination record.")?; + let path = path(project, &facet, layer) + .ok_or("Recovered attachment record has a different kind.")?; + let draft = Draft::from_recovery(project, saved)?; + let original = + Draft::from_project(project, id).ok_or("Recovered attachment is absent.")?; + let mut protected = draft.value.clone(); + *protected + .pointer_mut(&path) + .ok_or("Recovered attachment field is absent.")? = + original.value.pointer(&path).expect("Closed path").clone(); + if draft.original != id + || protected != original.value + || draft.inputs.iter().any(|(key, input)| { + key != &path + && original + .inputs + .get(key) + .is_none_or(|old| old.raw != input.raw) + }) + { + return Err( + "Recovered attachment draft changes identity, kind or an unrelated property." + .into(), + ); + } + if editor.drafts.insert(facet, draft).is_some() { + return Err("Recovered attachment draft address is duplicated.".into()); + } + } + Ok(editor) + } +} + +#[cfg(test)] +#[path = "attachments_tests.rs"] +mod tests; diff --git a/crates/oa-desktop/src/semantic_review/attachments_tests.rs b/crates/oa-desktop/src/semantic_review/attachments_tests.rs new file mode 100644 index 00000000..c8adb9de --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/attachments_tests.rs @@ -0,0 +1,795 @@ +//! Exact attachment adapters, addressed session composition, and recovery oracles. +use super::*; +struct Fixture { + p: Project, + owners: [Id; 2], + records: [Id; 2], + outside: [Id; 2], + trace: Id, + facet: FacetSelector, +} +fn fixture(child: bool, end: IncidentEnd) -> Fixture { + let mut p = Project::empty(); + let mut owners = vec![]; + let mut records = vec![]; + let mut outside = vec![]; + for layer in [Layer::Oa, Layer::Sa] { + let kind = if child { + if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + } + } else { + Kind::DataType + }; + let owner = p.add_element(kind, layer, "Current owner", None).unwrap(); + let other = p.add_element(kind, layer, "Final owner", None).unwrap(); + let record = if child { + p.add_element(kind, layer, "Child", Some(owner)).unwrap() + } else { + let peer = p.add_element(kind, layer, "Other endpoint", None).unwrap(); + let (a, b) = if end == IncidentEnd::Source { + (owner, peer) + } else { + (peer, owner) + }; + p.relate(RelationKind::DataAssociation, "Association", a, b) + }; + owners.push(owner); + records.push(record); + outside.push(other); + } + for element in p.elements.values_mut() { + if let Some(SharedBody::DataType { definition, .. }) = element.shared_mut() { + *definition = DataTypeDefinition::Record { supertype: None }; + } + } + let trace = Id::new_v4(); + p.traces.insert( + trace, + Trace { + id: trace, + alias: "attachment_trace".into(), + name: "Attachment rationale".into(), + description: String::new(), + oa: owners[0], + sa: owners[1], + kind: TraceKind::Manual, + baseline: None, + }, + ); + let keys = KeyPair { + oa: Some(records[0]), + sa: Some(records[1]), + }; + let facet = if child { + FacetSelector::Child(keys) + } else { + FacetSelector::Incident { keys, end } + }; + validate_candidate(&p).unwrap(); + Fixture { + p, + owners: [owners[0], owners[1]], + records: [records[0], records[1]], + outside: [outside[0], outside[1]], + trace, + facet, + } +} +fn request(f: &Fixture, reverse: bool, decision: SemanticDecision) -> SemanticTransitionRequest { + let source = usize::from(reverse); + let target = 1 - source; + let direction = if reverse { + TransitionDirection::SaToOa + } else { + TransitionDirection::OaToSa + }; + let mut transition = TransitionRequest::new(direction, vec![f.owners[source]]); + transition.mappings.push(TransitionMapping { + source: f.owners[source], + target: Some(f.owners[target]), + target_kind: None, + }); + let mut r = SemanticTransitionRequest::new(transition); + r.traces.insert(f.owners[source], f.trace); + r.references + .extend([(f.owners[0], f.owners[1]), (f.records[0], f.records[1])]); + r.reviews.push(SemanticReviewRequest { + source: f.owners[source], + resolutions: vec![FacetResolution { + facet: f.facet.clone(), + decision, + }], + reset: vec![], + }); + r +} +fn typed(f: &Fixture, record: Value) -> SemanticRecord { + if matches!(f.facet, FacetSelector::Child(_)) { + SemanticRecord::Element(serde_json::from_value(record).unwrap()) + } else { + SemanticRecord::Relation(serde_json::from_value(record).unwrap()) + } +} +#[test] +fn parent_and_each_endpoint_candidate_preserve_other_fields_and_match_core_both_directions() { + for child in [true, false] { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + if child && end == IncidentEnd::Target { + continue; + } + for reverse in [false, true] { + let f = fixture(child, end); + let r = request(&f, reverse, SemanticDecision::Candidate); + let direction = r.transition.direction; + let layer = direction.target_layer(); + let target = usize::from(!reverse); + let mut editor = Editor::default(); + editor.ensure(&f.p, &f.facet, layer).unwrap(); + let path = path(&f.p, &f.facet, layer).unwrap(); + editor + .drafts + .get_mut(&f.facet) + .unwrap() + .shape_scoped(&path, serde_json::json!(f.outside[target])) + .unwrap(); + let proposal = editor + .propose( + &f.p, + &f.p.traces[&f.trace], + direction, + &f.facet, + SemanticDecision::Candidate, + false, + ) + .unwrap(); + assert_eq!(proposal.slot, actual_slot(&f.facet, layer).unwrap()); + assert_eq!(proposal.path, path); + let proposed = typed(&f, proposal.record); + let mut r = r; + r.candidates.push(proposed.clone()); + let plan = plan_semantic_transition(&f.p, r).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let result = prepare_semantic_transition(&f.p, &plan).unwrap().project; + assert_eq!(semantic_record(&result, f.records[target]), Some(proposed)); + assert_eq!( + semantic_record(&result, f.records[1 - target]), + semantic_record(&f.p, f.records[1 - target]) + ); + assert_eq!( + result.traces.len(), + 1, + "The attached record needs no invented trace" + ); + } + } + } +} +#[test] +fn source_only_absence_and_explicit_absent_endpoint_are_different() { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + for reverse in [false, true] { + let mut f = fixture(false, end); + let r = request(&f, reverse, SemanticDecision::TakeSource); + let direction = r.transition.direction; + let source = usize::from(reverse); + let target = 1 - source; + let relation = f.p.relations.get_mut(&f.records[source]).unwrap(); + let (a, b) = relation.body.endpoints(); + relation.body.set_endpoints( + if end == IncidentEnd::Source { + f.outside[source] + } else { + a + }, + if end == IncidentEnd::Target { + f.outside[source] + } else { + b + }, + ); + let editor = Editor::default(); + assert!( + editor + .propose( + &f.p, + &f.p.traces[&f.trace], + direction, + &f.facet, + SemanticDecision::TakeSource, + false + ) + .err() + .unwrap() + .contains("explicit replacement") + ); + let FacetSelector::Incident { keys, .. } = &mut f.facet else { + unreachable!() + }; + if reverse { + keys.sa = None; + } else { + keys.oa = None; + } + let proposal = editor + .propose( + &f.p, + &f.p.traces[&f.trace], + direction, + &f.facet, + SemanticDecision::TakeSource, + false, + ) + .unwrap(); + assert_eq!( + typed(&f, proposal.record), + semantic_record(&f.p, f.records[target]).unwrap() + ); + } + } +} +#[test] +fn attachment_pair_drafts_survive_family_switch_and_restore_without_mutating_model() { + let f = fixture(false, IncidentEnd::Source); + let mut editor = Editor::default(); + editor.select_pair(&f.p, &f.facet).unwrap(); + editor + .update(&f.p, &f.p.traces[&f.trace], Event::Family(Family::Target)) + .unwrap(); + editor + .update( + &f.p, + &f.p.traces[&f.trace], + Event::Pick(Layer::Sa, Some(view::choice(&f.p, f.records[1]))), + ) + .unwrap(); + editor + .update(&f.p, &f.p.traces[&f.trace], Event::Family(Family::Source)) + .unwrap(); + let staged = editor + .update(&f.p, &f.p.traces[&f.trace], Event::Stage) + .unwrap() + .unwrap(); + assert_eq!(staged, f.facet); + let saved = serde_json::to_value(editor.recovery_snapshot()).unwrap(); + let restored = Editor::from_recovery( + &f.p, + Layer::Sa, + serde_json::from_value(saved.clone()).unwrap(), + ) + .unwrap(); + assert_eq!( + serde_json::to_value(restored.recovery_snapshot()).unwrap(), + saved + ); + assert_eq!(restored.selection(Layer::Oa).unwrap().id, f.records[0]); + assert!( + editor + .choices(&f.p, &f.p.traces[&f.trace], Layer::Sa) + .iter() + .any(|choice| choice.id == f.records[1]) + ); +} +#[test] +fn raw_rejected_attachment_reference_survives_recovery_and_wrong_scope_is_rejected() { + let f = fixture(true, IncidentEnd::Source); + let mut editor = Editor::default(); + editor.ensure(&f.p, &f.facet, Layer::Sa).unwrap(); + editor + .drafts + .get_mut(&f.facet) + .unwrap() + .input("/parent", "-".into()); + let saved = serde_json::to_value(editor.recovery_snapshot()).unwrap(); + let mut restored = Editor::from_recovery( + &f.p, + Layer::Sa, + serde_json::from_value(saved.clone()).unwrap(), + ) + .unwrap(); + assert_eq!(restored.drafts[&f.facet].inputs["/parent"].raw, "-"); + let proposed = restored + .propose( + &f.p, + &f.p.traces[&f.trace], + TransitionDirection::OaToSa, + &f.facet, + SemanticDecision::Candidate, + false, + ) + .unwrap(); + assert!(serde_json::from_value::(proposed.record).is_err()); + restored + .drafts + .get_mut(&f.facet) + .unwrap() + .input("/parent", f.outside[1].to_string()); + let repaired = restored + .propose( + &f.p, + &f.p.traces[&f.trace], + TransitionDirection::OaToSa, + &f.facet, + SemanticDecision::Candidate, + false, + ) + .unwrap(); + assert_eq!( + serde_json::from_value::(repaired.record) + .unwrap() + .parent, + Some(f.outside[1]) + ); + let mut duplicate = saved.clone(); + let row = duplicate["drafts"][0].clone(); + duplicate["drafts"].as_array_mut().unwrap().push(row); + assert!( + Editor::from_recovery(&f.p, Layer::Sa, serde_json::from_value(duplicate).unwrap()).is_err() + ); + let mut changed = saved; + changed["drafts"][0][1]["value"]["name"] = serde_json::json!("Unrelated changed name"); + assert!( + Editor::from_recovery(&f.p, Layer::Sa, serde_json::from_value(changed).unwrap()).is_err() + ); +} + +fn send(state: &mut State, p: &Project, event: RowEvent) { + let event = state.address_event(event); + state.update(p, event); +} +fn session(f: &Fixture, reverse: bool) -> State { + let mut state = State::new(&f.p, f.trace).unwrap(); + if reverse { + state.update( + &f.p, + super::super::Event::Direction(TransitionDirection::SaToOa), + ); + } + send(&mut state, &f.p, RowEvent::EditPair(f.facet.clone())); + send(&mut state, &f.p, RowEvent::Attachment(Event::Stage)); + assert_eq!(state.references.get(&f.records[0]), Some(&f.records[1])); + state +} +fn candidate(state: &mut State, p: &Project, facet: &FacetSelector, value: Id) { + let path = path(p, facet, state.direction.target_layer()).unwrap(); + send(state, p, RowEvent::Select(facet.clone())); + send( + state, + p, + RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)), + ); + send(state, p, RowEvent::Shape(path, serde_json::json!(value))); +} +fn errors(state: &State) -> String { + format!( + "{:?} / {:?}", + state.error, + state.plan.as_ref().map(|p| &p.conflicts) + ) +} +#[test] +fn addressed_attachment_candidates_equal_direct_core_oracle_both_directions() { + for child in [true, false] { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + if child && end == IncidentEnd::Target { + continue; + } + for reverse in [false, true] { + let f = fixture(child, end); + let target = usize::from(!reverse); + let mut state = session(&f, reverse); + candidate(&mut state, &f.p, &f.facet, f.outside[target]); + assert!(state.can_apply(), "{}", errors(&state)); + let mut direct = request(&f, reverse, SemanticDecision::Candidate); + let mut record = Draft::from_project(&f.p, f.records[target]).unwrap().value; + *record + .pointer_mut(&path(&f.p, &f.facet, state.direction.target_layer()).unwrap()) + .unwrap() = serde_json::json!(f.outside[target]); + direct.candidates.push(typed(&f, record)); + let plan = plan_semantic_transition(&f.p, direct).unwrap(); + let expected = prepare_semantic_transition(&f.p, &plan).unwrap().project; + assert_eq!(state.proposed.as_ref(), Some(&expected)); + assert!(state.command(&f.p).is_ok()); + assert_eq!( + f.p.traces[&f.trace].baseline, None, + "Preview cannot write source history" + ); + } + } + } +} +#[test] +fn attachment_candidate_and_direct_record_keep_conflict_on_actual_slot_without_losing_drafts() { + for child in [true, false] { + for end in [IncidentEnd::Source, IncidentEnd::Target] { + if child && end == IncidentEnd::Target { + continue; + } + for reverse in [false, true] { + let mut f = fixture(child, end); + let trace = super::super::tests::add_trace( + &mut f.p, + f.records[0], + f.records[1], + "Direct record rationale", + ); + let mut state = session(&f, reverse); + candidate(&mut state, &f.p, &f.facet, f.outside[usize::from(!reverse)]); + let authored = state.active_address.unwrap(); + state.update( + &f.p, + super::super::Event::ChooseTrace(view::choice(&f.p, trace)), + ); + state.update(&f.p, super::super::Event::OpenTrace); + let (_, slot) = actual_slot(&f.facet, state.direction.target_layer()).unwrap(); + send( + &mut state, + &f.p, + RowEvent::Decide(slot, Some(SemanticDecision::KeepTarget)), + ); + assert!(!state.can_apply()); + assert!( + errors(&state).contains("Independent row proposals disagree"), + "{}", + errors(&state) + ); + assert!( + state + .row(authored) + .unwrap() + .attachments + .drafts + .contains_key(&f.facet) + ); + let saved = state.recovery_snapshot().unwrap(); + let restored = State::from_recovery(&f.p, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + assert!(!restored.can_apply()); + } + } + } +} +#[test] +fn two_incident_endpoint_candidates_form_one_complete_record_and_recover_invalid_excluded_draft() { + let mut f = fixture(false, IncidentEnd::Source); + // Both ends are incident to the reviewed owner, so source and target are two + // independent attachment addresses on the same actual relationship. + for i in 0..2 { + f.p.relations + .get_mut(&f.records[i]) + .unwrap() + .body + .set_endpoints(f.owners[i], f.owners[i]); + } + let other = FacetSelector::Incident { + keys: KeyPair { + oa: Some(f.records[0]), + sa: Some(f.records[1]), + }, + end: IncidentEnd::Target, + }; + let mut state = session(&f, false); + candidate(&mut state, &f.p, &f.facet, f.outside[1]); + send(&mut state, &f.p, RowEvent::EditPair(other.clone())); + send(&mut state, &f.p, RowEvent::Attachment(Event::Stage)); + candidate(&mut state, &f.p, &other, f.outside[1]); + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + state.proposed.as_ref().unwrap().relations[&f.records[1]] + .body + .endpoints(), + (f.outside[1], f.outside[1]) + ); + let path = path(&f.p, &other, Layer::Sa).unwrap(); + send(&mut state, &f.p, RowEvent::Input(path.clone(), "-".into())); + assert!(!state.can_apply()); + let address = state.active_address.unwrap(); + state.update(&f.p, super::super::Event::Include(address, false)); + let saved = state.recovery_snapshot().unwrap(); + let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + assert!(!restored.included.contains(&address)); + assert_eq!(restored.attachments.drafts[&other].inputs[&path].raw, "-"); + restored.update(&f.p, super::super::Event::Include(address, true)); + assert!(!restored.can_apply()); + send( + &mut restored, + &f.p, + RowEvent::Input(path, f.outside[1].to_string()), + ); + assert!(restored.can_apply(), "{}", errors(&restored)); +} +#[test] +fn attachment_apply_is_one_exact_source_and_history_undo_redo() { + use crate::app::App; + let f = fixture(true, IncidentEnd::Source); + let mut app = App::with_project(f.p.clone()); + let mut bundle = app.document.bundle().clone(); + bundle + .files + .get_mut(&bundle.entry.clone()) + .unwrap() + .insert_str(0, "// Child allocation rationale stays exact\r\n"); + app.document.apply_bundle(bundle).unwrap(); + let before = app.document.snapshot(); + super::super::tests::start(&mut app, f.trace); + super::super::tests::send(&mut app, RowEvent::EditPair(f.facet.clone())); + super::super::tests::send(&mut app, RowEvent::Attachment(Event::Stage)); + super::super::tests::decide(&mut app, f.facet.clone(), SemanticDecision::Candidate); + super::super::tests::send( + &mut app, + RowEvent::Shape("/parent".into(), serde_json::json!(f.outside[1])), + ); + let state = super::super::tests::state(&app); + assert!(state.can_apply(), "{}", errors(state)); + let expected = state.proposed.clone().unwrap(); + super::super::tests::apply(&mut app); + assert_eq!(app.project(), &expected); + let after = app.document.snapshot(); + assert!( + after.bundle.files[&after.bundle.entry] + .starts_with("// Child allocation rationale stays exact\r\n") + ); + let _ = app.update(Message::Undo); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + let _ = app.update(Message::Redo); + assert_eq!(app.project(), &expected); + assert_eq!(app.document.bundle(), &after.bundle); +} + +#[test] +fn absent_attachment_payloads_migrate_v1_and_v2_and_source5_upgrade_requires_exact_legacy_base() { + let f = fixture(true, IncidentEnd::Source); + let mut state = State::new(&f.p, f.trace).unwrap(); + let facet = FacetSelector::Header(HeaderFacet::Name); + send(&mut state, &f.p, RowEvent::Select(facet.clone())); + send( + &mut state, + &f.p, + RowEvent::Decide(facet, Some(SemanticDecision::Candidate)), + ); + send( + &mut state, + &f.p, + RowEvent::Input("/name".into(), "Retained exact name".into()), + ); + for mut value in [ + state.active.recovery_snapshot().unwrap(), + state.recovery_snapshot().unwrap(), + ] { + if value["version"] == 1 { + value.as_object_mut().unwrap().remove("attachments"); + } else { + for row in value["rows"].as_array_mut().unwrap() { + row["draft"].as_object_mut().unwrap().remove("attachments"); + } + } + let restored = State::from_recovery(&f.p, value.clone()).unwrap(); + assert!(restored.attachments.drafts.is_empty()); + assert_eq!( + restored.draft.as_ref().unwrap().inputs["/name"].raw, + "Retained exact name" + ); + value["base_fingerprint"] = serde_json::json!("verified legacy source-5 fingerprint"); + assert!(State::from_recovery(&f.p, value.clone()).is_err()); + assert!( + State::from_recovery_after_source5_upgrade(&f.p, "wrong base", value.clone()).is_err() + ); + let upgraded = State::from_recovery_after_source5_upgrade( + &f.p, + "verified legacy source-5 fingerprint", + value.clone(), + ) + .unwrap(); + assert_eq!(upgraded.base_fingerprint, project_fingerprint(&f.p)); + assert_eq!(upgraded.decisions, restored.decisions); + assert_eq!( + upgraded.draft.as_ref().unwrap().inputs["/name"].raw, + "Retained exact name" + ); + assert!(upgraded.accepted_impacts.is_empty()); + value["base_identity"] = serde_json::json!(Id::new_v4()); + assert!( + State::from_recovery_after_source5_upgrade( + &f.p, + "verified legacy source-5 fingerprint", + value + ) + .is_err() + ); + } +} + +#[test] +fn functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate() { + let mut p = Project::empty(); + let mut owners = vec![]; + let mut replacements = vec![]; + let mut exchanges = vec![]; + for layer in [Layer::Oa, Layer::Sa] { + let kind = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + let owner = p.add_element(kind, layer, "Sender", None).unwrap(); + let receiver = p.add_element(kind, layer, "Receiver", None).unwrap(); + let replacement = p + .add_element(kind, layer, "Reviewed replacement sender", None) + .unwrap(); + let exchange = p.relate( + if layer == Layer::Oa { + RelationKind::Interaction + } else { + RelationKind::FunctionalExchange + }, + "Payload exchange", + owner, + receiver, + ); + owners.push(owner); + replacements.push(replacement); + exchanges.push(exchange); + } + let port = Id::new_v4(); + p.elements.insert( + port, + make_port( + &p, + port, + replacements[1], + "Reviewed replacement output", + PortContract::FunctionOutput { + outgoing_items: vec![], + }, + ) + .unwrap(), + ); + let owner_trace = + super::super::tests::add_trace(&mut p, owners[0], owners[1], "Sender ownership rationale"); + let exchange_trace = super::super::tests::add_trace( + &mut p, + exchanges[0], + exchanges[1], + "Explicit exchange port rationale", + ); + validate_candidate(&p).unwrap(); + let before = p.clone(); + let facet = FacetSelector::Incident { + keys: KeyPair { + oa: Some(exchanges[0]), + sa: Some(exchanges[1]), + }, + end: IncidentEnd::Source, + }; + let mut state = State::new(&p, owner_trace).unwrap(); + send(&mut state, &p, RowEvent::EditPair(facet.clone())); + send(&mut state, &p, RowEvent::Attachment(Event::Stage)); + candidate(&mut state, &p, &facet, replacements[1]); + assert!(!state.can_apply()); + assert!( + errors(&state).contains("functional_port"), + "{}", + errors(&state) + ); + assert!( + state.plan.as_ref().unwrap().impacts.is_empty(), + "Matching ports are explicit review choices, never invented impacts" + ); + + state.update( + &p, + super::super::Event::ChooseTrace(view::choice(&p, exchange_trace)), + ); + state.update(&p, super::super::Event::OpenTrace); + // The direct Source agrees with the owner's Incident Source. It also gives + // the direct row's port picker the explicitly authored replacement owner. + for (selector, value) in [ + (RelationFacet::Source, replacements[1]), + (RelationFacet::SourcePort, port), + ] { + let selector = FacetSelector::Relation(selector); + send(&mut state, &p, RowEvent::Select(selector.clone())); + send( + &mut state, + &p, + RowEvent::Decide(selector.clone(), Some(SemanticDecision::Candidate)), + ); + let field = scalar::binding(&p, exchanges[1], &selector).unwrap(); + if selector == FacetSelector::Relation(RelationFacet::SourcePort) { + let draft = state.candidate_draft(&selector).unwrap(); + assert!( + crate::forms::reference_choices(draft, &p, &field, Id::nil()) + .iter() + .any(|choice| choice.id == port), + "The actual scoped picker must offer the matching explicitly owned port" + ); + } + send( + &mut state, + &p, + RowEvent::Shape(field, serde_json::json!(value)), + ); + } + assert!(state.can_apply(), "{}", errors(&state)); + assert!(state.plan.as_ref().unwrap().impacts.is_empty()); + let request = &state.plan.as_ref().unwrap().request; + assert_eq!( + request.candidates.len(), + 1, + "Two owner records must not become two conflicting partial relationship candidates" + ); + assert_eq!(request.reviews.len(), 2); + assert!( + request + .reviews + .iter() + .find(|review| review.source == exchanges[0]) + .unwrap() + .resolutions + .iter() + .any(|resolution| resolution.facet + == FacetSelector::Relation(RelationFacet::SourcePort) + && resolution.decision == SemanticDecision::Candidate) + ); + + let mut transition = + TransitionRequest::new(TransitionDirection::OaToSa, vec![owners[0], exchanges[0]]); + transition.mappings = vec![ + TransitionMapping { + source: owners[0], + target: Some(owners[1]), + target_kind: None, + }, + TransitionMapping { + source: exchanges[0], + target: Some(exchanges[1]), + target_kind: None, + }, + ]; + let mut direct = SemanticTransitionRequest::new(transition); + direct.references = BTreeMap::from([(owners[0], owners[1]), (exchanges[0], exchanges[1])]); + direct.traces = BTreeMap::from([(owners[0], owner_trace), (exchanges[0], exchange_trace)]); + direct.reviews = vec![ + SemanticReviewRequest { + source: owners[0], + resolutions: vec![FacetResolution { + facet, + decision: SemanticDecision::Candidate, + }], + reset: vec![], + }, + SemanticReviewRequest { + source: exchanges[0], + resolutions: [RelationFacet::Source, RelationFacet::SourcePort] + .into_iter() + .map(|facet| FacetResolution { + facet: FacetSelector::Relation(facet), + decision: SemanticDecision::Candidate, + }) + .collect(), + reset: vec![], + }, + ]; + let mut relation = p.relations[&exchanges[1]].clone(); + let (_, receiver) = relation.body.endpoints(); + relation.body.set_endpoints(replacements[1], receiver); + if let RelationBody::FunctionalExchange { source_port, .. } = &mut relation.body { + *source_port = port; + } else { + unreachable!(); + } + direct.candidates.push(SemanticRecord::Relation(relation)); + let plan = plan_semantic_transition(&p, direct).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let expected = prepare_semantic_transition(&p, &plan).unwrap().project; + assert_eq!(state.proposed.as_ref(), Some(&expected)); + assert!(state.command(&p).is_ok()); + assert_eq!( + p, before, + "Combined preview never mutates accepted ports or model history" + ); +} diff --git a/crates/oa-desktop/src/semantic_review/attachments_view.rs b/crates/oa-desktop/src/semantic_review/attachments_view.rs new file mode 100644 index 00000000..e1351835 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/attachments_view.rs @@ -0,0 +1,79 @@ +//! Explicit attachment pair and final destination field controls. +use super::{ + attachments::{Editor, Event, Family}, + *, +}; +use iced::{ + Element as View, Fill, + widget::{button, column, pick_list, row, text, text_input}, +}; + +pub(super) fn pairs<'a>( + editor: &'a Editor, + project: &'a Project, + trace: &'a Trace, +) -> View<'a, Event> { + let mut content = column![ + text("Existing children and relationship endpoints").size(16), + text("Choose explicit Operational and System identities. A child's final parent or a relationship's final endpoint is reviewed independently; opening another trace keeps this draft.").size(12), + pick_list([Family::Child, Family::Source, Family::Target], Some(editor.family), Event::Family).text_size(12).padding(7).width(Fill), + text_input("Filter attachment identities by name or alias", &editor.search).on_input(Event::Search).size(12).padding(7), + ].spacing(9); + for layer in [Layer::Oa, Layer::Sa] { + let label = if layer == Layer::Oa { + "Operational" + } else { + "System" + }; + let owner = if layer == Layer::Oa { + trace.oa + } else { + trace.sa + }; + content = content.push(text(format!("{label} owner: {}", project.named(owner))).size(12)); + content = content.push( + row![ + pick_list( + editor.choices(project, trace, layer), + editor.selection(layer).cloned(), + move |choice| Event::Pick(layer, Some(choice)) + ) + .placeholder(format!("Choose an existing {label} identity")) + .text_size(12) + .padding(7) + .width(Fill), + button(text("No identity on this side").size(11)) + .style(crate::style::nav) + .padding(7) + .on_press(Event::Pick(layer, None)), + ] + .spacing(7), + ); + } + content = content.push( + button(text("Use exact attachment pair").size(12)) + .padding(8) + .style(crate::style::nav) + .on_press(Event::Stage), + ); + content.push(text("Taking an absent relationship endpoint requires an explicit final replacement. New child and relationship creation uses a separate explicit transition mapping.").size(11)).padding(iced::Padding::default().right(18)).into() +} + +pub(super) fn candidate<'a>( + editor: &'a Editor, + project: &'a Project, + facet: &'a FacetSelector, + layer: Layer, +) -> Option> { + let draft = editor.drafts.get(facet)?; + let (id, _) = attachments::actual_slot(facet, layer)?; + let path = attachments::path(project, facet, layer)?; + Some( + column![ + text(format!("Destination record: {}", project.named(id))).size(12), + crate::forms::scoped(draft, project, &path, attachments::label(facet)), + ] + .spacing(7) + .into(), + ) +} diff --git a/crates/oa-desktop/src/semantic_review/batch.rs b/crates/oa-desktop/src/semantic_review/batch.rs new file mode 100644 index 00000000..0a69684f --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/batch.rs @@ -0,0 +1,689 @@ +//! Session orchestration. Row focus never changes the identity of authored drafts. +use super::*; +use serde::{Deserialize, Serialize}; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; +pub(super) const MAX_REVIEW_ROWS: usize = 256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RowAddress { + pub source: Id, + pub target: Id, + pub trace: Id, +} +impl RowAddress { + pub(super) fn from_trace(trace: &Trace, direction: TransitionDirection) -> Self { + let (source, target) = match direction { + TransitionDirection::OaToSa => (trace.oa, trace.sa), + TransitionDirection::SaToOa => (trace.sa, trace.oa), + }; + Self { + source, + target, + trace: trace.id, + } + } + pub(super) fn direction(self, project: &Project) -> Result { + let trace = project + .traces + .get(&self.trace) + .ok_or("Review trace no longer exists.")?; + if self.source == trace.oa && self.target == trace.sa { + Ok(TransitionDirection::OaToSa) + } else if self.source == trace.sa && self.target == trace.oa { + Ok(TransitionDirection::SaToOa) + } else { + Err("Review row and its exact trace correspondence disagree.".into()) + } + } +} + +#[derive(Debug, Clone)] +pub enum Event { + AcceptImpact(Box, bool), + ChooseTrace(Choice), + OpenTrace, + Focus(RowAddress), + Include(RowAddress, bool), + Direction(TransitionDirection), + Row { + address: Option, + facet: Option, + event: RowEvent, + }, + // Intermediate row-view message, always addressed by State::view before reaching App. + Editor(RowEvent), +} +#[cfg(test)] +impl From for Event { + fn from(event: RowEvent) -> Self { + Self::Editor(event) + } +} + +pub struct State { + pub(super) base: Arc, + pub(super) base_identity: Id, + pub(super) base_fingerprint: String, + pub direction: TransitionDirection, + pub references: BTreeMap, + pub plan: Option, + pub proposed: Option, + pub error: Option, + pub(super) accepted_impacts: Vec, + pub(super) active: ReviewRow, + pub(super) active_address: Option, + pub(super) bank: BTreeMap, + pub(super) order: Vec, + pub(super) included: BTreeSet, + pub(super) add_trace: Option, + pub(super) notice: Option, +} +// The active editor remains accessible to existing local property tests/helpers. +// Session planning/error/reference fields deliberately take precedence over row context. +impl Deref for State { + type Target = ReviewRow; + fn deref(&self) -> &ReviewRow { + &self.active + } +} +impl DerefMut for State { + fn deref_mut(&mut self) -> &mut ReviewRow { + &mut self.active + } +} + +impl State { + pub fn new(project: &Project, selected: Id) -> Result { + let active = ReviewRow::new(project, selected)?; + let direction = active.direction; + let active_address = active + .trace + .and_then(|id| project.traces.get(&id)) + .map(|trace| RowAddress::from_trace(trace, direction)); + let mut state = Self { + base: Arc::new(project.clone()), + base_identity: project.id, + base_fingerprint: project_fingerprint(project), + direction, + references: BTreeMap::new(), + plan: None, + proposed: None, + error: None, + accepted_impacts: vec![], + active, + active_address, + bank: BTreeMap::new(), + order: active_address.into_iter().collect(), + included: active_address.into_iter().collect(), + add_trace: None, + notice: None, + }; + state.refresh(project); + Ok(state) + } + pub fn target(&self, _project: &Project) -> Option { + self.active_address.map(|address| address.target) + } + pub(super) fn row(&self, address: RowAddress) -> Option<&ReviewRow> { + if self.active_address == Some(address) { + Some(&self.active) + } else { + self.bank.get(&address) + } + } + fn row_mut(&mut self, address: Option) -> Option<&mut ReviewRow> { + if self.active_address == address { + Some(&mut self.active) + } else { + address.and_then(|address| self.bank.get_mut(&address)) + } + } + pub(super) fn included_addresses(&self) -> Vec { + self.included + .iter() + .filter(|address| address.direction(&self.base).ok() == Some(self.direction)) + .copied() + .collect() + } + pub(super) fn frozen(&self, project: &Project) -> Result<(), String> { + if project.id != self.base_identity || project_fingerprint(project) != self.base_fingerprint + { + Err("This review is stale: the accepted model changed after this review began. Drafts are retained; reopen a review against the new model before applying.".into()) + } else { + Ok(()) + } + } + pub fn address_event(&self, event: RowEvent) -> Event { + if let RowEvent::AcceptImpact(index, accepted) = &event + && let Some(impact) = self.plan.as_ref().and_then(|plan| plan.impacts.get(*index)) + { + return Event::AcceptImpact(Box::new(impact.clone()), *accepted); + } + Event::Row { + address: self.active_address, + facet: self.active.selected.clone(), + event, + } + } + #[cfg(test)] + pub(super) fn stage_row_references(&mut self, facet: &FacetSelector) -> Result<(), String> { + self.active.stage_row_references(facet)?; + self.references.extend(self.active.references.clone()); + Ok(()) + } + pub(super) fn canonical_references(&self) -> Result, String> { + let mut references = self.references.clone(); + let mut sources = BTreeSet::new(); + for address in self.included_addresses() { + if !sources.insert(address.source) { + return Err("Only one trace row per source can be included. Exclude its alternative history explicitly.".into()); + } + let trace = &self.base.traces[&address.trace]; + if references + .insert(trace.oa, trace.sa) + .is_some_and(|old| old != trace.sa) + { + return Err(format!( + "Transition mapping disagrees with frozen correspondence: {}. Reverse convergence of multiple System sources on one Operational target is not representable by the core's canonical reference map. Exclude a row or choose a distinct target; all drafts are retained.", + self.base.named(trace.oa) + )); + } + } + Ok(references) + } + fn open(&mut self, trace_id: Id, replace_active: bool) -> Result<(), String> { + let trace = self + .base + .traces + .get(&trace_id) + .ok_or("Choose an existing trace.")?; + let address = RowAddress::from_trace(trace, self.direction); + if self.active_address == Some(address) { + return Ok(()); + } + let old = self.active_address; + let transfer = replace_active && old.is_some_and(|old| self.included.contains(&old)); + let existed = self.bank.contains_key(&address); + if !existed && self.order.len() >= MAX_REVIEW_ROWS { + return Err("Semantic review exceeds the supported 256 retained rows.".into()); + } + let mut next = if let Some(row) = self.bank.remove(&address) { + row + } else { + let mut row = ReviewRow::new(&self.base, trace_id)?; + row.direction = self.direction; + row.load_trace(&self.base); + row + }; + // References are a derived editor context; the session retains authored pairs. + next.references = self.references.clone(); + let previous = std::mem::replace(&mut self.active, next); + if let Some(old) = old { + self.bank.insert(old, previous); + } + self.active_address = Some(address); + if !self.order.contains(&address) { + self.order.push(address); + } + if transfer && let Some(old) = old { + self.included.remove(&old); + } + let occupied = self + .included_addresses() + .iter() + .any(|other| other.source == address.source && *other != address); + if !occupied && (transfer || !existed) { + self.included.insert(address); + } + if occupied { + self.notice = Some("This source already has an included history. The alternative draft is open but excluded; exclude the other row before including this one.".into()); + } + Ok(()) + } + fn change_direction( + &mut self, + direction: TransitionDirection, + trace: Option, + ) -> Result<(), String> { + let previous = self.direction; + self.direction = direction; + if let Some(trace) = trace { + if let Err(error) = self.open(trace, false) { + self.direction = previous; + return Err(error); + } + } else { + self.active.direction = direction; + } + Ok(()) + } + fn focus(&mut self, address: RowAddress) -> Result<(), String> { + if self.active_address == Some(address) { + return Ok(()); + } + let next = self + .bank + .remove(&address) + .ok_or("That review draft is no longer available.")?; + let previous = std::mem::replace(&mut self.active, next); + if let Some(old) = self.active_address { + self.bank.insert(old, previous); + } + self.active_address = Some(address); + self.direction = address.direction(&self.base)?; + Ok(()) + } + fn include(&mut self, address: RowAddress, include: bool) -> Result<(), String> { + if self.row(address).is_none() { + return Err("That review row is unavailable.".into()); + } + if include { + let direction = address.direction(&self.base)?; + if self.included.iter().any(|other| { + other != &address + && other.source == address.source + && other.direction(&self.base).ok() == Some(direction) + }) { + return Err("Only one included trace per source is allowed. Exclude the alternative history first; both drafts are retained.".into()); + } + self.included.insert(address); + } else { + self.included.remove(&address); + } + Ok(()) + } + pub fn update(&mut self, project: &Project, event: impl Into) { + let event = event.into(); + #[cfg(test)] + let event = match event { + Event::Editor(event) => self.address_event(event), + other => other, + }; + if let Err(error) = self.frozen(project) { + // Withdrawing a pending reset is authored intent only. Retain it while + // keeping the original accepted base and disabling every stale plan. + if let Event::Row { + address, + event: RowEvent::Reset(facet, false), + .. + } = &event + && let Some(row) = self.row_mut(*address) + && row.resets.remove(facet) + { + self.accepted_impacts.clear(); + } + self.error = Some(error.clone()); + self.active.error = Some(error); + self.plan = None; + self.proposed = None; + return; + } + let base = Arc::clone(&self.base); + let browser_only = matches!( + &event, + Event::Row { + event: RowEvent::ValueBrowse(_, _, _) | RowEvent::ReferenceBrowse(_, _, _), + .. + } + ); + self.notice = None; + let mut authored = true; + let result = match event { + Event::AcceptImpact(impact, accepted) => { + authored = false; + if !self + .plan + .as_ref() + .is_some_and(|plan| plan.impacts.contains(&impact)) + { + Err("This exact prepared change is no longer in the preview. Review the current changes before accepting.".into()) + } else { + self.accepted_impacts + .retain(|old| old.object != impact.object); + if accepted { + self.accepted_impacts.push(*impact); + } + Ok(()) + } + } + Event::ChooseTrace(choice) => { + self.add_trace = Some(choice); + authored = false; + Ok(()) + } + Event::OpenTrace => self + .add_trace + .as_ref() + .map(|choice| choice.id) + .ok_or_else(|| "Choose a trace to add or reopen.".to_owned()) + .and_then(|id| self.open(id, false)), + Event::Focus(address) => { + authored = false; + self.focus(address) + } + Event::Include(address, include) => self.include(address, include), + Event::Direction(direction) => self.change_direction(direction, self.active.trace), + Event::Editor(_) => { + Err("Unaddressed editor message was rejected; choose its review row again.".into()) + } + Event::Row { + address, + facet, + event, + } => { + if self.row_mut(address).is_none() { + Err("The originating review draft is no longer available.".into()) + } else { + match event { + #[cfg(test)] + RowEvent::Trace(choice) => { + if self.active_address != address { + Err("A delayed trace selection belongs to another row; reopen that row first.".into()) + } else { + self.open(choice.id, true) + } + } + #[cfg(test)] + RowEvent::Direction(direction) => { + let trace = self.row_mut(address).and_then(|row| row.trace); + self.change_direction(direction, trace) + } + RowEvent::AddReference => { + let row = self.row_mut(address).expect("Checked row"); + match (&row.reference_oa, &row.reference_sa) { + (Some(oa), Some(sa)) => { + let (oa, sa) = (oa.id, sa.id); + self.references.insert(oa, sa); + Ok(()) + } + _ => { + Err("Choose both an Operational and a System reference.".into()) + } + } + } + RowEvent::RestoreReviewedReferences => { + let pairs = history::reviewed_pairs( + self.row_mut(address).expect("Checked row"), + &base, + ); + pairs.and_then(|pairs| { + let mut merged = self.canonical_references()?; + for (oa, sa) in &pairs { + if merged.insert(*oa, *sa).is_some_and(|old| old != *sa) { + return Err("A reviewed pair conflicts with the current explicit correspondence. No pairs were restored.".into()); + } + } + if self.direction == TransitionDirection::SaToOa && merged.values().collect::>().len() != merged.len() { + return Err("Reviewed pairs would make reverse correspondence ambiguous. No pairs were restored.".into()); + } + self.notice = Some(format!("Restored {} exact historical key pairs. Order observations alone do not establish correspondence.", pairs.len())); + self.references.extend(pairs); + Ok(()) + }) + } + RowEvent::RemoveReference(id) => { + self.references.remove(&id); + Ok(()) + } + RowEvent::AcceptImpact(_, _) => { + authored = false; + Err("The prepared change could not be identified exactly; refresh its preview.".into()) + } + event => { + authored = !matches!( + event, + RowEvent::Select(_) + | RowEvent::Tab(_) + | RowEvent::ShowUnchanged(_) + | RowEvent::ReferenceOa(_) + | RowEvent::ReferenceSa(_) + | RowEvent::ReferenceSearch(_) + | RowEvent::Attachment( + attachments::Event::Family(_) + | attachments::Event::Pick(_, _) + | attachments::Event::Search(_) + ) + | RowEvent::Artifact( + artifacts::Event::Pick(_, _) | artifacts::Event::Search(_) + ) + | RowEvent::MemberFamily(_) + | RowEvent::MemberOa(_) + | RowEvent::MemberSa(_) + | RowEvent::Literal(_) + | RowEvent::LiteralSide(_, _) + | RowEvent::EditPair(_) + | RowEvent::AnnotationAddress(_, _) + | RowEvent::AnnotationBrowse(_, _, _) + | RowEvent::ValueBrowse(_, _, _) + | RowEvent::ReferenceBrowse(_, _, _) + | RowEvent::Preview + ); + let scoped_edit = matches!( + event, + RowEvent::Input(_, _) + | RowEvent::Text(_, _) + | RowEvent::Shape(_, _) + | RowEvent::Variant(_, _) + | RowEvent::Array(_, _, _) + | RowEvent::Append(_, _) + | RowEvent::ValuePath(_, _) + | RowEvent::ValueBrowse(_, _, _) + | RowEvent::ReferenceBrowse(_, _, _) + | RowEvent::OrderMove(_, _, _) + | RowEvent::OrderReconcile(_) + ); + let stage = matches!( + event, + RowEvent::StageMember + | RowEvent::Attachment(attachments::Event::Stage) + | RowEvent::Artifact(artifacts::Event::Stage) + ); + let row = self.row_mut(address).expect("Checked row"); + let prior_references = stage.then(|| row.references.clone()); + let selected = row.selected.clone(); + let previous_error = row.error.take(); + if scoped_edit { + row.selected = facet; + } + row.update(&base, event); + if scoped_edit { + row.selected = selected; + } + let result = row.error.clone().map_or(Ok(()), Err); + if browser_only && result.is_ok() { + row.error = previous_error; + } + if let Some(prior) = prior_references + && result.is_ok() + { + let changed: Vec<_> = row + .references + .iter() + .filter(|(id, value)| prior.get(id) != Some(value)) + .map(|(id, value)| (*id, *value)) + .collect(); + self.references.extend(changed); + } + result + } + } + } + } + }; + if authored { + self.accepted_impacts.clear(); + } + if !browser_only { + self.refresh(project); + } + if let Err(error) = result { + self.error = Some(error.clone()); + self.active.error = Some(error); + } + } + pub(super) fn request(&self, _project: &Project) -> Result { + compose::request(self) + } + pub fn refresh(&mut self, project: &Project) { + self.plan = None; + self.proposed = None; + self.error = None; + if let Err(error) = self.frozen(project) { + self.error = Some(error); + return; + } + let base = Arc::clone(&self.base); + let references = self.canonical_references(); + let context = references.as_ref().unwrap_or(&self.references).clone(); + for row in self + .bank + .values_mut() + .chain(std::iter::once(&mut self.active)) + { + row.references = context.clone(); + if let Some(trace) = row.trace.and_then(|id| base.traces.get(&id)) { + row.references.insert(trace.oa, trace.sa); + } + row.refresh(&base); + } + let request = references.and_then(|_| self.request(&base)); + match request.and_then(|request| { + plan_semantic_transition(&base, request).map_err(|error| error.to_string()) + }) { + Ok(plan) => { + if plan.conflicts.is_empty() { + match prepare_semantic_transition(&base, &plan) { + Ok(prepared) => self.proposed = Some(prepared.project), + Err(error) => self.error = Some(error.to_string()), + } + } + self.plan = Some(plan); + } + Err(error) => self.error = Some(error), + } + self.active.plan = self.plan.clone(); + self.active.proposed = self.proposed.clone(); + self.active.accepted_impacts = self.accepted_impacts.clone(); + self.active.error = self.error.clone().or_else(|| { + self.active + .combined_value(&base, false) + .err() + .map(|error| format!("This row's retained draft: {error}")) + }); + if let Some(value) = self + .proposed + .as_ref() + .and_then(|project| { + self.target(project) + .and_then(|id| Draft::from_project(project, id)) + }) + .map(|draft| draft.value) + { + if let Some(draft) = &mut self.active.draft { + draft.set_context(Some(value.clone())); + } + for draft in self.active.group_drafts.values_mut() { + draft.set_context(Some(value.clone())); + } + } + } + pub fn command(&mut self, project: &Project) -> Result { + self.frozen(project)?; + if !self.has_decisions() { + return Err("Choose at least one included property or exact history reset.".into()); + } + if let Some(error) = &self.error { + return Err(error.clone()); + } + let plan = self + .plan + .as_ref() + .ok_or("Refresh the combined review preview before applying.")?; + prepare_semantic_transition(project, plan).map_err(|error| error.to_string())?; + Ok(Command::Batch(plan.commands.clone())) + } + fn has_decisions(&self) -> bool { + self.included_addresses() + .into_iter() + .filter_map(|address| self.row(address)) + .any(|row| !row.decisions.is_empty() || !row.resets.is_empty()) + } + pub fn can_apply(&self) -> bool { + self.has_decisions() + && self.error.is_none() + && self.plan.as_ref().is_some_and(|plan| { + plan.conflicts.is_empty() && plan.prepared_fingerprint.is_some() + }) + } + pub fn view<'a>(&'a self, _project: &'a Project) -> iced::Element<'a, Message> { + use iced::{ + Fill, + widget::{button, checkbox, column, pick_list, row, text}, + }; + let message = |event| Message::Workflow(crate::workflow::Event::Semantic(event)); + let mut available: Vec<_> = self + .base + .traces + .values() + .map(|trace| Choice { + id: trace.id, + label: format!( + "{} · {} ↔ {}", + trace.name, + self.base.named(trace.oa), + self.base.named(trace.sa) + ), + }) + .collect(); + available.sort_by(|a, b| a.label.cmp(&b.label)); + let choices: Vec<_> = self + .order + .iter() + .filter(|address| address.direction(&self.base).ok() == Some(self.direction)) + .map(|address| Choice { + id: address.trace, + label: format!( + "{} · {} → {} · {}", + if self.included.contains(address) { + "Included" + } else { + "Excluded" + }, + self.base.named(address.source), + self.base.named(address.target), + self.base.named(address.trace) + ), + }) + .collect(); + let selected = choices + .iter() + .find(|choice| { + self.active_address + .is_some_and(|address| address.trace == choice.id) + }) + .cloned(); + let direction = self.direction; + let base = &self.base; + let mut header = column![ + text(format!("Review semantics · {} included rows", self.included_addresses().len())).size(22), + row![pick_list(available, self.add_trace.clone(), move |choice| message(Event::ChooseTrace(choice))).placeholder("Choose another trace to add or reopen").width(Fill).text_size(12).padding(7), button(text("Open row").size(12)).padding(7).style(crate::style::nav).on_press(message(Event::OpenTrace)), button(text(if direction == TransitionDirection::OaToSa { "OA → SA" } else { "SA → OA" }).size(12)).padding(7).style(crate::style::nav).on_press(message(Event::Direction(if direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa })))].spacing(7), + row![pick_list(choices, selected, move |choice| message(Event::Focus(RowAddress::from_trace(&base.traces[&choice.id], direction)))).placeholder("Choose a trace / review history").width(Fill).text_size(12).padding(7), checkbox(self.active_address.is_some_and(|address| self.included.contains(&address))).label("Include this row").on_toggle_maybe(self.active_address.map(|address| move |included| message(Event::Include(address, included))))].spacing(10), + text("Switching rows retains every draft. Only included rows contribute to the one combined preview.").size(10).color(crate::style::MUTED), + ].spacing(6); + if let Some(notice) = &self.notice { + header = header.push(text(notice).size(11)); + } + self.active + .view(&self.base, header.into(), self.can_apply()) + .map(move |message| match message { + Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event))) => { + Message::Workflow(crate::workflow::Event::Semantic(self.address_event(event))) + } + other => other, + }) + } +} diff --git a/crates/oa-desktop/src/semantic_review/batch_tests.rs b/crates/oa-desktop/src/semantic_review/batch_tests.rs new file mode 100644 index 00000000..3cee5d5a --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/batch_tests.rs @@ -0,0 +1,807 @@ +use super::*; +use crate::{app::App, document::Document}; + +struct Ranges { + project: Project, + sources: [Id; 2], + targets: [Id; 2], + traces: [Id; 2], + direction: TransitionDirection, +} +fn ranges(reverse: bool, converge: bool) -> Ranges { + let mut project = Project::empty(); + let direction = if reverse { + TransitionDirection::SaToOa + } else { + TransitionDirection::OaToSa + }; + let mut add = |name, layer| { + let id = project + .add_element(Kind::DataType, layer, name, None) + .unwrap(); + *project.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType { + definition: DataTypeDefinition::Integer { + minimum: Some(0), + maximum: Some(100), + }, + default: None, + unit: "degrees".into(), + }; + id + }; + let sources = [ + add("Normal operation", direction.source_layer()), + add("Emergency operation", direction.source_layer()), + ]; + let first = add("Controller limit", direction.target_layer()); + let second = if converge { + first + } else { + add("Backup controller limit", direction.target_layer()) + }; + let targets = [first, second]; + let traces = std::array::from_fn(|i| { + let (oa, sa) = if reverse { + (targets[i], sources[i]) + } else { + (sources[i], targets[i]) + }; + tests::add_trace( + &mut project, + oa, + sa, + if i == 0 { + "Normal limit rationale" + } else { + "Emergency limit rationale" + }, + ) + }); + Ranges { + project, + sources, + targets, + traces, + direction, + } +} +fn address(f: &Ranges, i: usize) -> RowAddress { + RowAddress { + source: f.sources[i], + target: f.targets[i], + trace: f.traces[i], + } +} +fn send(state: &mut State, project: &Project, event: RowEvent) { + let event = state.address_event(event); + state.update(project, event); +} +fn open(state: &mut State, project: &Project, trace: Id) { + state.update(project, Event::ChooseTrace(view::choice(project, trace))); + state.update(project, Event::OpenTrace); +} +fn path(direction: TransitionDirection, leaf: &str) -> String { + format!( + "/body/{}/Shared/DataType/definition/Integer/{leaf}", + if direction.target_layer() == Layer::Oa { + "Oa" + } else { + "Sa" + } + ) +} +fn set_bound(state: &mut State, f: &Ranges, facet: DataFacet, raw: &str) { + let selector = FacetSelector::Data(facet); + send(state, &f.project, RowEvent::Select(selector.clone())); + send( + state, + &f.project, + RowEvent::Decide(selector, Some(SemanticDecision::Candidate)), + ); + send( + state, + &f.project, + RowEvent::Input( + path( + f.direction, + if facet == DataFacet::IntegerMinimum { + "minimum" + } else { + "maximum" + }, + ), + raw.into(), + ), + ); +} +fn bounds(project: &Project, target: Id) -> (Option, Option) { + match project.elements[&target].shared().unwrap() { + SharedBody::DataType { + definition: DataTypeDefinition::Integer { minimum, maximum }, + .. + } => (*minimum, *maximum), + other => panic!("Unexpected target {other:?}"), + } +} +fn errors(state: &State) -> String { + format!( + "{:?} / {:?}", + state.error, + state.plan.as_ref().map(|plan| &plan.conflicts) + ) +} + +#[test] +fn batch_joint_bounds_use_one_candidate_and_match_an_independent_atomic_core_oracle() { + let f = ranges(false, true); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "120"); + assert!(!state.can_apply(), "A single 120..100 row is invalid"); + open(&mut state, &f.project, f.traces[1]); + set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150"); + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + bounds(state.proposed.as_ref().unwrap(), f.targets[0]), + (Some(120), Some(150)) + ); + let mut transition = TransitionRequest::new(f.direction, f.sources.to_vec()); + transition.mappings = (0..2) + .map(|i| TransitionMapping { + source: f.sources[i], + target: Some(f.targets[i]), + target_kind: None, + }) + .collect(); + let mut expected = SemanticTransitionRequest::new(transition); + expected.references = (0..2).map(|i| (f.sources[i], f.targets[i])).collect(); + expected.traces = (0..2).map(|i| (f.sources[i], f.traces[i])).collect(); + expected.reviews = [DataFacet::IntegerMinimum, DataFacet::IntegerMaximum] + .into_iter() + .enumerate() + .map(|(i, facet)| SemanticReviewRequest { + source: f.sources[i], + resolutions: vec![FacetResolution { + facet: FacetSelector::Data(facet), + decision: SemanticDecision::Candidate, + }], + reset: vec![], + }) + .collect(); + let mut candidate = f.project.elements[&f.targets[0]].clone(); + let SharedBody::DataType { definition, .. } = candidate.shared_mut().unwrap() else { + unreachable!() + }; + *definition = DataTypeDefinition::Integer { + minimum: Some(120), + maximum: Some(150), + }; + expected.candidates.push(SemanticRecord::Element(candidate)); + let oracle = plan_semantic_transition(&f.project, expected).unwrap(); + assert!(oracle.conflicts.is_empty(), "{:?}", oracle.conflicts); + assert_eq!( + state.proposed.as_ref().unwrap(), + &prepare_semantic_transition(&f.project, &oracle) + .unwrap() + .project + ); + assert_eq!(state.plan.as_ref().unwrap().request.candidates.len(), 1); + let prepared = state.proposed.clone(); + state.order.reverse(); + state.refresh(&f.project); + assert_eq!( + state.proposed, prepared, + "Display order must not choose the winning draft" + ); +} + +#[test] +fn batch_candidate_conflict_keeps_both_raw_values_and_keep_is_a_real_constraint() { + let f = ranges(false, true); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + open(&mut state, &f.project, f.traces[1]); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "7"); + assert!(!state.can_apply()); + let error = errors(&state); + assert!( + error.contains("Normal operation") + && error.contains("Emergency operation") + && error.contains("disagree"), + "{error}" + ); + let minimum = path(f.direction, "minimum"); + assert_eq!( + state + .row(address(&f, 0)) + .unwrap() + .draft + .as_ref() + .unwrap() + .inputs[&minimum] + .raw, + "5" + ); + assert_eq!( + state + .row(address(&f, 1)) + .unwrap() + .draft + .as_ref() + .unwrap() + .inputs[&minimum] + .raw, + "7" + ); + send( + &mut state, + &f.project, + RowEvent::Decide( + FacetSelector::Data(DataFacet::IntegerMinimum), + Some(SemanticDecision::KeepTarget), + ), + ); + assert!( + !state.can_apply(), + "Keeping zero must conflict with another row's five" + ); + send( + &mut state, + &f.project, + RowEvent::Decide(FacetSelector::Data(DataFacet::IntegerMinimum), None), + ); + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + bounds(state.proposed.as_ref().unwrap(), f.targets[0]).0, + Some(5) + ); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + assert!( + state.can_apply(), + "Equal independently authored values agree" + ); +} + +#[test] +fn batch_excluded_invalid_row_survives_focus_reopen_and_v2_recovery() { + let f = ranges(false, true); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + open(&mut state, &f.project, f.traces[1]); + set_bound(&mut state, &f, DataFacet::IntegerMaximum, "-"); + assert!(!state.can_apply()); + state.update(&f.project, Event::Include(address(&f, 1), false)); + assert!(state.can_apply(), "{}", errors(&state)); + state.update(&f.project, Event::Focus(address(&f, 0))); + open(&mut state, &f.project, f.traces[1]); + assert!( + !state.included.contains(&address(&f, 1)), + "Reopen is focus, not consent to include invalid work" + ); + assert!(state.can_apply(), "{}", errors(&state)); + send( + &mut state, + &f.project, + RowEvent::Select(FacetSelector::Data(DataFacet::IntegerMinimum)), + ); + assert!( + state.can_apply(), + "Selecting an excluded property must not promote its draft error to a batch failure" + ); + let saved = state.recovery_snapshot().unwrap(); + assert_eq!(saved["version"], 2); + let restored = State::from_recovery(&f.project, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + assert_eq!( + restored + .row(address(&f, 1)) + .unwrap() + .draft + .as_ref() + .unwrap() + .inputs[&path(f.direction, "maximum")] + .raw, + "-" + ); + assert!(restored.can_apply(), "{}", errors(&restored)); + assert_eq!(restored.proposed, state.proposed); +} + +#[test] +fn batch_delayed_text_targets_its_original_row_and_facet_without_switching_focus() { + let f = ranges(false, true); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + let late = state.address_event(RowEvent::Input(path(f.direction, "minimum"), "6".into())); + open(&mut state, &f.project, f.traces[1]); + set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150"); + state.update(&f.project, late); + assert_eq!(state.active_address, Some(address(&f, 1))); + assert_eq!( + state.selected, + Some(FacetSelector::Data(DataFacet::IntegerMaximum)) + ); + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + bounds(state.proposed.as_ref().unwrap(), f.targets[0]), + (Some(6), Some(150)) + ); + state.update(&f.project, Event::Focus(address(&f, 0))); + let late = state.address_event(RowEvent::Input(path(f.direction, "minimum"), "8".into())); + set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150"); + state.update(&f.project, late); + assert_eq!( + state.selected, + Some(FacetSelector::Data(DataFacet::IntegerMaximum)) + ); + assert_eq!( + state.draft.as_ref().unwrap().inputs[&path(f.direction, "minimum")].raw, + "8" + ); + assert_eq!( + state.draft.as_ref().unwrap().inputs[&path(f.direction, "maximum")].raw, + "150" + ); +} + +#[test] +fn batch_reverse_distinct_targets_work_and_reverse_convergence_is_explicit() { + for converge in [false, true] { + let f = ranges(true, converge); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + open(&mut state, &f.project, f.traces[1]); + set_bound(&mut state, &f, DataFacet::IntegerMaximum, "150"); + if converge { + assert!(!state.can_apply()); + assert!( + errors(&state).contains("Reverse convergence"), + "{}", + errors(&state) + ); + assert_eq!(state.included_addresses().len(), 2); + } else { + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + bounds(state.proposed.as_ref().unwrap(), f.targets[0]).0, + Some(5) + ); + assert_eq!( + bounds(state.proposed.as_ref().unwrap(), f.targets[1]).1, + Some(150) + ); + assert_eq!(state.plan.as_ref().unwrap().request.candidates.len(), 2); + } + } +} + +#[test] +fn batch_v2_rejects_duplicate_included_sources_bad_addresses_and_stale_model() { + let mut f = ranges(false, false); + let alternate = tests::add_trace( + &mut f.project, + f.sources[0], + f.targets[0], + "Alternative rationale", + ); + let mut state = State::new(&f.project, f.traces[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + open(&mut state, &f.project, alternate); + let alternate_address = RowAddress { + trace: alternate, + ..address(&f, 0) + }; + assert!(!state.included.contains(&alternate_address)); + let saved = state.recovery_snapshot().unwrap(); + let mut duplicate = saved.clone(); + duplicate["rows"][1]["included"] = Value::Bool(true); + assert!( + State::from_recovery(&f.project, duplicate) + .err() + .unwrap() + .contains("multiple included") + ); + let mut duplicate = saved.clone(); + let extra = duplicate["rows"][0].clone(); + duplicate["rows"].as_array_mut().unwrap().push(extra); + assert!(State::from_recovery(&f.project, duplicate).is_err()); + let mut bad = saved.clone(); + bad["active"]["target"] = serde_json::to_value(f.sources[0]).unwrap(); + assert!(State::from_recovery(&f.project, bad).is_err()); + let mut changed = f.project.clone(); + changed.name.push_str(" newer accepted revision"); + assert!(State::from_recovery(&changed, saved).is_err()); + assert!(state.command(&changed).is_err()); + let before = state + .row(address(&f, 0)) + .unwrap() + .draft + .as_ref() + .unwrap() + .inputs[&path(f.direction, "minimum")] + .raw + .clone(); + state.update(&changed, Event::Focus(address(&f, 0))); + assert_eq!( + state + .row(address(&f, 0)) + .unwrap() + .draft + .as_ref() + .unwrap() + .inputs[&path(f.direction, "minimum")] + .raw, + before + ); +} + +#[test] +fn batch_constructor_retirement_conflicts_with_another_rows_kept_bound_in_both_orders() { + for first in [0, 1] { + let mut f = ranges(false, true); + *f.project + .elements + .get_mut(&f.targets[0]) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::DataType { + definition: DataTypeDefinition::Real { + minimum: Some(0.0), + maximum: Some(100.0), + }, + default: None, + unit: "degrees".into(), + }; + let mut state = State::new(&f.project, f.sources[first]).unwrap(); + for i in [first, 1 - first] { + if state.active_address != Some(address(&f, i)) { + open(&mut state, &f.project, f.traces[i]); + } + let facet = FacetSelector::Data(if i == 0 { + DataFacet::Constructor + } else { + DataFacet::RealMinimum + }); + send(&mut state, &f.project, RowEvent::Select(facet.clone())); + send( + &mut state, + &f.project, + RowEvent::Decide( + facet, + Some(if i == 0 { + SemanticDecision::TakeSource + } else { + SemanticDecision::KeepTarget + }), + ), + ); + } + assert!( + !state.can_apply(), + "A constructor must not retire another row's kept bound" + ); + assert!(errors(&state).contains("disagree"), "{}", errors(&state)); + assert!( + state + .row(address(&f, 0)) + .unwrap() + .decisions + .contains_key(&FacetSelector::Data(DataFacet::Constructor)) + ); + assert!( + state + .row(address(&f, 1)) + .unwrap() + .decisions + .contains_key(&FacetSelector::Data(DataFacet::RealMinimum)) + ); + } +} + +#[test] +fn batch_apply_is_one_exact_source_and_history_undo_redo_roundtrip() { + let f = ranges(false, true); + let mut app = App::with_project(f.project.clone()); + let mut bundle = app.document.bundle().clone(); + bundle + .files + .get_mut(&bundle.entry.clone()) + .unwrap() + .insert_str( + 0, + "// Two independent engineering rationales stay verbatim\r\n", + ); + app.document.apply_bundle(bundle).unwrap(); + let before = app.document.snapshot(); + tests::start(&mut app, f.sources[0]); + tests::decide( + &mut app, + FacetSelector::Data(DataFacet::IntegerMinimum), + SemanticDecision::Candidate, + ); + tests::send( + &mut app, + RowEvent::Input(path(f.direction, "minimum"), "120".into()), + ); + let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic( + Event::ChooseTrace(view::choice(&f.project, f.traces[1])), + ))); + let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic( + Event::OpenTrace, + ))); + tests::decide( + &mut app, + FacetSelector::Data(DataFacet::IntegerMaximum), + SemanticDecision::Candidate, + ); + tests::send( + &mut app, + RowEvent::Input(path(f.direction, "maximum"), "150".into()), + ); + assert!( + tests::state(&app).can_apply(), + "{}", + errors(tests::state(&app)) + ); + let expected = tests::state(&app).proposed.clone().unwrap(); + tests::apply(&mut app); + assert_eq!(app.project(), &expected); + let after = app.document.snapshot(); + assert!( + after.bundle.files[&after.bundle.entry] + .starts_with("// Two independent engineering rationales stay verbatim\r\n") + ); + let _ = app.update(Message::Undo); + assert_eq!(app.project(), &before.decoded.project); + assert_eq!(app.document.bundle(), &before.bundle); + let _ = app.update(Message::Redo); + assert_eq!(app.project(), &expected); + assert_eq!(app.document.bundle(), &after.bundle); + assert_eq!( + Document::from_bundle( + after.bundle.clone(), + source_project::ProjectLimits::default() + ) + .unwrap() + .project(), + &expected + ); +} + +#[test] +fn category_family_and_members_are_independent_core_checked_facets_both_directions() { + for reverse in [false, true] { + let mut project = Project::empty(); + let mut owners = vec![]; + let mut exchanges = vec![]; + for layer in [Layer::Oa, Layer::Sa] { + let work = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + let source = project + .add_element(work, layer, "Issue command", None) + .unwrap(); + let target = project + .add_element(work, layer, "Perform command", None) + .unwrap(); + let exchange = project.relate( + if layer == Layer::Oa { + RelationKind::Interaction + } else { + RelationKind::FunctionalExchange + }, + "Command", + source, + target, + ); + let owner = project + .add_element(Kind::ExchangeCategory, layer, "Command concern", None) + .unwrap(); + *project + .elements + .get_mut(&owner) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::ExchangeCategory { + family: oa_core::view_options::ExchangeFamily::Work, + exchanges: vec![exchange], + }; + owners.push(owner); + exchanges.push(exchange); + } + tests::add_trace(&mut project, owners[0], owners[1], "Concern correspondence"); + validate_candidate(&project).unwrap(); + let mut state = State::new(&project, owners[usize::from(reverse)]).unwrap(); + state.references.insert(exchanges[0], exchanges[1]); + state.refresh(&project); + let member = FacetSelector::Member { + collection: ReferenceCollection::CategoryExchanges, + keys: KeyPair { + oa: Some(exchanges[0]), + sa: Some(exchanges[1]), + }, + }; + let family = FacetSelector::Category(CategoryFacet::Family); + assert!(state.editable(&project, &family)); + assert!(state.editable(&project, &member)); + send(&mut state, &project, RowEvent::Select(family.clone())); + send( + &mut state, + &project, + RowEvent::Decide(family.clone(), Some(SemanticDecision::Candidate)), + ); + let target = owners[usize::from(!reverse)]; + let path = scalar::binding(&project, target, &family).unwrap(); + send( + &mut state, + &project, + RowEvent::Shape(path, serde_json::json!("Participant")), + ); + assert!( + !state.can_apply(), + "Work exchanges cannot belong to a Participant category" + ); + send(&mut state, &project, RowEvent::Select(member.clone())); + send( + &mut state, + &project, + RowEvent::Decide(member.clone(), Some(SemanticDecision::Candidate)), + ); + send(&mut state, &project, RowEvent::Presence(member, false)); + assert!(state.can_apply(), "{}", errors(&state)); + assert!( + matches!(state.proposed.as_ref().unwrap().elements[&target].shared().unwrap(), SharedBody::ExchangeCategory { family: oa_core::view_options::ExchangeFamily::Participant, exchanges } if exchanges.is_empty()) + ); + assert_eq!( + prepare_command(&project, &state.command(&project).unwrap()) + .unwrap() + .project, + state.proposed.clone().unwrap() + ); + let _ = state.view(&project); + } +} + +#[test] +fn rejected_direction_switch_at_row_limit_preserves_every_recoverable_draft() { + let mut f = ranges(false, true); + let mut traces = vec![f.traces[0]]; + for i in 1..batch::MAX_REVIEW_ROWS { + traces.push(tests::add_trace( + &mut f.project, + f.sources[0], + f.targets[0], + &format!("Alternative {i}"), + )); + } + let mut state = State::new(&f.project, traces[0]).unwrap(); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "-"); + for trace in traces.into_iter().skip(1) { + let row = ReviewRow::new(&f.project, trace).unwrap(); + let address = RowAddress { + source: f.sources[0], + target: f.targets[0], + trace, + }; + state.bank.insert(address, row); + state.order.push(address); + } + let before = state.recovery_snapshot().unwrap(); + for event in [ + Event::Direction(TransitionDirection::SaToOa), + state.address_event(RowEvent::Direction(TransitionDirection::SaToOa)), + ] { + state.update(&f.project, event); + assert_eq!(state.direction, TransitionDirection::OaToSa); + assert!(state.error.as_ref().unwrap().contains("256")); + let saved = state.recovery_snapshot().unwrap(); + assert_eq!(saved, before); + let restored = State::from_recovery(&f.project, saved).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), before); + assert_eq!( + restored.draft.as_ref().unwrap().inputs[&path(f.direction, "minimum")].raw, + "-" + ); + } +} + +#[test] +fn batch_complete_constructor_cannot_erase_another_rows_new_literal() { + let mut f = ranges(false, true); + for id in f.sources.into_iter().chain(f.targets) { + *f.project + .elements + .get_mut(&id) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::DataType { + definition: DataTypeDefinition::Enumeration { + literals: vec!["A".into()], + }, + default: None, + unit: String::new(), + }; + } + let constructor = FacetSelector::Data(DataFacet::Constructor); + let mut first = State::new(&f.project, f.sources[0]).unwrap(); + send( + &mut first, + &f.project, + RowEvent::Decide(constructor.clone(), Some(SemanticDecision::KeepTarget)), + ); + assert!(first.can_apply(), "{}", errors(&first)); + f.project = first.proposed.unwrap(); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + send( + &mut state, + &f.project, + RowEvent::Decide(constructor, Some(SemanticDecision::TakeSource)), + ); + open(&mut state, &f.project, f.traces[1]); + let literal = FacetSelector::Literal(KeyPair { + oa: None, + sa: Some("B".into()), + }); + send(&mut state, &f.project, RowEvent::Select(literal.clone())); + send( + &mut state, + &f.project, + RowEvent::Decide(literal.clone(), Some(SemanticDecision::Candidate)), + ); + send(&mut state, &f.project, RowEvent::Presence(literal, true)); + assert!(!state.can_apply()); + assert!(errors(&state).contains("disagree"), "{}", errors(&state)); + state.update(&f.project, Event::Include(address(&f, 0), false)); + assert!(state.can_apply(), "{}", errors(&state)); + assert!( + matches!(state.proposed.as_ref().unwrap().elements[&f.targets[0]].shared().unwrap(), SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, .. } if literals == &vec!["A".to_string(), "B".to_string()]) + ); +} + +#[test] +fn batch_first_equal_constructor_acknowledgement_allows_an_independent_bound() { + let f = ranges(false, true); + let mut state = State::new(&f.project, f.sources[0]).unwrap(); + send( + &mut state, + &f.project, + RowEvent::Decide( + FacetSelector::Data(DataFacet::Constructor), + Some(SemanticDecision::KeepTarget), + ), + ); + open(&mut state, &f.project, f.traces[1]); + set_bound(&mut state, &f, DataFacet::IntegerMinimum, "5"); + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!( + bounds(state.proposed.as_ref().unwrap(), f.targets[0]), + (Some(5), Some(100)) + ); +} + +#[test] +fn recovered_trace_picker_names_existing_trace_instead_of_deleted_reference() { + let f = ranges(false, true); + let mut state = State::new(&f.project, f.traces[0]).unwrap(); + state.update( + &f.project, + Event::ChooseTrace(view::choice(&f.project, f.traces[1])), + ); + let saved = state.recovery_snapshot().unwrap(); + let restored = State::from_recovery(&f.project, saved).unwrap(); + let selected = restored.add_trace.as_ref().unwrap(); + assert_eq!(selected.id, f.traces[1]); + assert!( + selected.label.contains("Emergency limit rationale"), + "{}", + selected.label + ); + assert!(!selected.label.contains("Deleted reference")); + assert_eq!(selected.label, state.add_trace.as_ref().unwrap().label); + assert!( + view::choice(&f.project, Id::new_v4()) + .label + .contains("Deleted reference") + ); +} diff --git a/crates/oa-desktop/src/semantic_review/compose.rs b/crates/oa-desktop/src/semantic_review/compose.rs new file mode 100644 index 00000000..cb79783d --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/compose.rs @@ -0,0 +1,626 @@ +//! Compare independent authored intent before building the core's unique target candidates. +use super::*; + +#[cfg(test)] +#[path = "compose_diagnostic_tests.rs"] +mod diagnostic_tests; + +type Slot = (Id, FacetSelector); +struct Intent { + value: Value, + row: RowAddress, + origin: FacetSelector, +} +struct Proposal { + address: RowAddress, + value: Value, + groups: Vec, + copies: Vec, +} + +fn canonical(facet: &FacetSelector, layer: Layer) -> Option { + fn key(keys: &KeyPair, layer: Layer) -> Option> { + Some(KeyPair { + oa: None, + sa: Some( + if layer == Layer::Oa { + keys.oa.as_ref()? + } else { + keys.sa.as_ref()? + } + .clone(), + ), + }) + } + Some(match facet { + FacetSelector::Member { collection, keys } => FacetSelector::Member { + collection: *collection, + keys: key(keys, layer)?, + }, + FacetSelector::Literal(keys) => FacetSelector::Literal(key(keys, layer)?), + FacetSelector::Annotation(keys) => FacetSelector::Annotation(key(keys, layer)?), + FacetSelector::Binding(keys) => FacetSelector::Binding(key(keys, layer)?), + FacetSelector::Child(_) + | FacetSelector::Incident { .. } + | FacetSelector::Artifact { .. } => return None, + _ => facet.clone(), + }) +} +fn observe(project: &Project, slot: &Slot) -> Result { + if let Some((_, facet)) = artifacts::parts(&slot.1) { + return Ok(artifacts::observation( + project.artifact_links.get(&slot.0), + facet, + )); + } + // A retired destination slot can legitimately apply on neither side of this + // synthetic target-only observation. Public capture_facet intentionally rejects + // such historical facts; determine absence through the same closed UI binding. + let applicable = if let Some(family) = keyed::family(&slot.1) { + keyed::path(&record_value(project, slot.0)?, family).is_some() + } else if let FacetSelector::Order(which) = slot.1 { + ordered::read(&record_value(project, slot.0)?, which).is_ok() + } else { + scalar::binding(project, slot.0, &slot.1).is_some() + }; + if !applicable { + return Ok(Value::String("NotApplicable".into())); + } + Ok(scalar::observation( + &capture_facet(project, slot.0, slot.0, &slot.1)?, + Layer::Sa, + )) +} +fn record_value(project: &Project, id: Id) -> Result { + Draft::from_project(project, id) + .map(|draft| draft.value) + .ok_or("Target has no editable semantic record.".into()) +} +fn install(project: &mut Project, id: Id, value: Value) -> Result<(), String> { + if let std::collections::btree_map::Entry::Occupied(mut entry) = project.elements.entry(id) { + let record: Element = serde_json::from_value(value) + .map_err(|error| format!("Final target value: {error}"))?; + if record.id != id { + return Err("Candidate identity changed.".into()); + } + entry.insert(record); + } else if let std::collections::btree_map::Entry::Occupied(mut entry) = + project.relations.entry(id) + { + let record: Relation = serde_json::from_value(value) + .map_err(|error| format!("Final target value: {error}"))?; + if record.id != id { + return Err("Candidate identity changed.".into()); + } + entry.insert(record); + } else if let std::collections::btree_map::Entry::Occupied(mut entry) = + project.artifact_links.entry(id) + { + let record: ArtifactLink = serde_json::from_value(value) + .map_err(|error| format!("Final artifact value: {error}"))?; + if record.id != id { + return Err("Candidate artifact identity changed.".into()); + } + entry.insert(record); + } else { + return Err("The batch foundation requires an existing target record.".into()); + } + Ok(()) +} +fn identity(base: &Project, id: Id) -> String { + base.identities() + .into_iter() + .find(|(key, _)| *key == id) + .map_or_else( + || format!("Missing identity {id}"), + |(_, alias)| format!("{} · @{alias}", base.named(id)), + ) +} +fn review_identity(state: &State, row: RowAddress, id: Id) -> String { + if !state.base.contains(id) + && let Some(review) = state.row(row) + && review.artifacts.seeds.contains_key(&id) + { + return review.review_choice(&state.base, id).label; + } + identity(&state.base, id) +} +fn review_title(state: &State, row: RowAddress, facet: &FacetSelector) -> String { + state.row(row).map_or_else( + || view::title(&state.base, facet), + |review| review.review_title(&state.base, facet), + ) +} +#[cfg(test)] +fn describe(base: &Project, slot: &Slot, value: &Value) -> String { + describe_with(slot, value, |id| identity(base, id)) +} +fn describe_with(slot: &Slot, value: &Value, identify: impl Fn(Id) -> String) -> String { + let text = if let Some((_, facet)) = artifacts::parts(&slot.1) { + if value.as_str() == Some("NotApplicable") { + "artifact absent".into() + } else if facet == ArtifactFacet::Owner { + value + .get("Value") + .and_then(Value::as_str) + .and_then(|s| s.parse::().ok()) + .map(|id| format!("owner {}", identify(id))) + .unwrap_or_else(|| value.to_string()) + } else { + value.get("Value").unwrap_or(value).to_string() + } + } else { + value.to_string() + }; + if text.chars().count() > 400 { + format!( + "{}… (full draft retained)", + text.chars().take(400).collect::() + ) + } else { + text + } +} +fn agree( + state: &State, + slots: &mut BTreeMap, + slot: Slot, + value: Value, + row: RowAddress, + origin: &FacetSelector, +) -> Result<(), String> { + let base = &state.base; + if let Some(previous) = slots.get(&slot) { + if previous.value != value { + let same_row = previous.row == row; + let heading = if same_row { + "Decisions in this review disagree" + } else { + "Independent row proposals disagree" + }; + let scope = artifacts::parts(&slot.1).map_or_else( + || view::title(base, &slot.1), + |(_, facet)| format!("Artifact {facet:?}"), + ); + let first_row = format!( + "{} → {} [trace {}]", + identity(base, previous.row.source), + identity(base, previous.row.target), + identity(base, previous.row.trace) + ); + let second_row = if same_row { + "the same review".into() + } else { + format!( + "{} → {} [trace {}]", + identity(base, row.source), + identity(base, row.target), + identity(base, row.trace) + ) + }; + let action = if same_row { + "Edit or unselect one of these decisions." + } else { + "Edit or unselect a decision, or exclude a row." + }; + return Err(format!( + "{heading} for {} · {scope}: {} in {first_row} requires {}; {} in {second_row} requires {}. {action} Both drafts are retained.", + review_identity(state, row, slot.0), + review_title(state, previous.row, &previous.origin), + describe_with(&slot, &previous.value, |id| review_identity( + state, + previous.row, + id + )), + review_title(state, row, origin), + describe_with(&slot, &value, |id| review_identity(state, row, id)) + )); + } + } else { + slots.insert( + slot, + Intent { + value, + row, + origin: origin.clone(), + }, + ); + } + Ok(()) +} +fn write_slot(project: &mut Project, slot: &Slot, observation: &Value) -> Result<(), String> { + if let Some((_, facet)) = artifacts::parts(&slot.1) { + if observation.as_str() == Some("NotApplicable") { + if facet == ArtifactFacet::Owner { + project.artifact_links.remove(&slot.0); + } + return Ok(()); + } + let mut value = record_value(project, slot.0)?; + let field = + artifacts::field(facet).ok_or("Artifact actual slot must identify metadata.")?; + value[field] = observation + .get("Value") + .cloned() + .ok_or("Artifact observation lacks typed value.")?; + return install(project, slot.0, value); + } + if scalar::grouped(&slot.1) || observation.as_str() == Some("NotApplicable") { + return Ok(()); + } + let supplied = observation + .get("Value") + .ok_or("Missing typed destination observation.")? + .clone(); + let mut value = record_value(project, slot.0)?; + if keyed_rows::supported(&slot.1) { + let key = keyed::key(&slot.1, Layer::Sa).ok_or("Destination row key missing.")?; + let path = keyed::path(&value, keyed::family(&slot.1).expect("Supported family")) + .ok_or("Destination row collection is no longer applicable.")?; + let rows = value + .pointer_mut(&path) + .and_then(Value::as_array_mut) + .ok_or("Destination rows missing.")?; + let index = rows.iter().position(|row| match &slot.1 { + FacetSelector::Annotation(_) => { + serde_json::json!({"path":row["path"],"field":row["field"]}) == key + } + FacetSelector::Binding(_) => row["referenced"] == key, + _ => false, + }); + match (index, supplied.is_null()) { + (Some(index), false) => rows[index] = supplied, + (Some(index), true) => { + rows.remove(index); + } + (None, false) => rows.push(supplied), + (None, true) => {} + } + } else if keyed::supported(&slot.1) { + keyed::project_presence( + &mut value, + &slot.1, + Layer::Sa, + supplied.as_bool().ok_or("Expected typed presence.")?, + )?; + } else if let FacetSelector::Order(which) = slot.1 { + let order: Vec = serde_json::from_value(supplied).map_err(|error| error.to_string())?; + let current = ordered::read(&value, which)?; + if order.len() != order.iter().collect::>().len() + || order.iter().collect::>() != current.iter().collect::>() + { + return Err("Combined ordered membership needs an explicit final target order containing each surviving member once. Review membership explicitly in the order's row.".into()); + } + let path = keyed::path(&value, keyed::Family::Member(ordered::collection(which))) + .ok_or("Order collection missing.")?; + *value.pointer_mut(&path).expect("Closed collection path") = + serde_json::to_value(order).expect("Order"); + } else { + let path = scalar::binding(project, slot.0, &slot.1) + .ok_or("A selected property was retired by another row's constructor proposal.")?; + *value + .pointer_mut(&path) + .ok_or("Selected property is no longer available.")? = supplied; + } + install(project, slot.0, value) +} + +pub(super) fn request(state: &State) -> Result { + let base = &state.base; + let addresses = state.included_addresses(); + if addresses.is_empty() { + return Err("Include at least one existing trace row in the combined review.".into()); + } + let references = state.canonical_references()?; + let mut transition = TransitionRequest::new( + state.direction, + addresses.iter().map(|address| address.source).collect(), + ); + transition.mappings = addresses + .iter() + .map(|address| TransitionMapping { + source: address.source, + target: Some(address.target), + target_kind: None, + }) + .collect(); + let mut request = SemanticTransitionRequest::new(transition); + request.references = references.clone(); + request.accepted_impacts = state.accepted_impacts.clone(); + let mut slots = BTreeMap::new(); + let mut proposals = vec![]; + let mut candidate_targets = BTreeSet::new(); + let mut implicit_orders = vec![]; + let mut artifact_creations = BTreeMap::new(); + for address in addresses { + let row = state + .row(address) + .ok_or("Included row draft is unavailable.")?; + let trace = row + .effective_trace(base) + .ok_or("Included trace is unavailable.")?; + request.traces.insert(address.source, address.trace); + request.reviews.push(SemanticReviewRequest { + source: address.source, + resolutions: row + .decisions + .iter() + .map(|(facet, decision)| FacetResolution { + facet: facet.clone(), + decision: *decision, + }) + .collect(), + reset: row.resets.iter().cloned().collect(), + }); + if row.decisions.iter().any(|(facet, decision)| { + !attachments::supported(facet) + && !artifacts::supported(facet) + && *decision == SemanticDecision::Candidate + }) { + candidate_targets.insert(address.target); + } + let final_value = row.combined_value(base, false).map_err(|error| { + format!( + "{} [{}]: {error}", + base.named(address.source), + base.named(address.trace) + ) + })?; + let mut proposed = (**base).clone(); + install(&mut proposed, address.target, final_value.clone())?; + let artifact = + row.artifacts + .propose(base, &trace, state.direction, &references, &row.decisions)?; + for (slot, observation, origin) in artifact.slots { + agree(state, &mut slots, slot, observation, address, &origin)?; + } + for seed in artifact.creations { + if artifact_creations + .insert(seed.id, seed.clone()) + .is_some_and(|prior| prior != seed) + { + return Err( + "Independent artifact creation records disagree. All drafts are retained." + .into(), + ); + } + } + candidate_targets.extend(artifact.candidates); + let mut groups = vec![]; + let mut copies = vec![]; + for (facet, decision) in &row.decisions { + if artifacts::supported(facet) { + continue; + } + if attachments::supported(facet) { + let attachment = row.attachments.propose( + base, + &trace, + state.direction, + facet, + *decision, + false, + )?; + let mut record = record_value(&proposed, attachment.slot.0)?; + *record + .pointer_mut(&attachment.path) + .ok_or("Attachment field was retired by another selected decision.")? = + attachment + .record + .pointer(&attachment.path) + .cloned() + .ok_or("Attachment proposal is incomplete.")?; + install(&mut proposed, attachment.slot.0, record)?; + let observed = observe(&proposed, &attachment.slot)?; + agree( + state, + &mut slots, + attachment.slot.clone(), + observed, + address, + facet, + )?; + if *decision == SemanticDecision::Candidate { + candidate_targets.insert(attachment.slot.0); + } + continue; + } + let Some(selector) = canonical(facet, state.direction.target_layer()) else { + continue; + }; + let slot = (address.target, selector); + let observed = if *decision == SemanticDecision::KeepTarget { + observe(base, &slot)? + } else { + observe(&proposed, &slot)? + }; + agree(state, &mut slots, slot.clone(), observed, address, facet)?; + let first_equal = row.can_acknowledge_constructor(base, facet) + && (*decision != SemanticDecision::Candidate + || observe(base, &slot)? == observe(&proposed, &slot)?); + if scalar::grouped(facet) && (*decision == SemanticDecision::Candidate || !first_equal) + { + copies.push(facet.clone()); + } + if scalar::grouped(facet) && first_equal && *decision == SemanticDecision::Candidate { + // Preserve authored payload changes even when the first equal-kind + // decision acknowledges only its discriminator. The core must see + // these changes and require separate descendant decisions. + let mut catalog = semantic_facets(base, &trace, &references)?; + catalog.extend(semantic_facets(&proposed, &trace, &references)?); + for fact in catalog { + if constructors::contains(facet, &fact.selector) + && let Some(selector) = + canonical(&fact.selector, state.direction.target_layer()) + { + let slot = (address.target, selector); + let supplied = observe(&proposed, &slot)?; + if supplied != observe(base, &slot)? { + agree(state, &mut slots, slot, supplied, address, facet)?; + } + } + } + } + if scalar::grouped(facet) && !first_equal { + groups.push(facet.clone()); + let own = if *decision == SemanticDecision::KeepTarget { + &**base + } else { + &proposed + }; + let mut catalog = semantic_facets(base, &trace, &references)?; + catalog.extend(semantic_facets(own, &trace, &references)?); + for fact in catalog { + if constructors::contains(facet, &fact.selector) + && let Some(selector) = + canonical(&fact.selector, state.direction.target_layer()) + { + let slot = (address.target, selector); + agree( + state, + &mut slots, + slot.clone(), + observe(own, &slot)?, + address, + facet, + )?; + } + } + } + } + for which in [ + OrderedCollection::Steps, + OrderedCollection::FragmentBranches, + ] { + if !row.decisions.contains_key(&FacetSelector::Order(which)) + && let Ok(order) = ordered::read(&record_value(&proposed, address.target)?, which) + && ordered::read(&record_value(base, address.target)?, which) + .ok() + .as_ref() + != Some(&order) + { + implicit_orders.push((address.target, which, order)); + } + } + proposals.push(Proposal { + address, + value: final_value, + groups, + copies, + }); + } + // Recheck every selected slot against every complete constructor scope, including + // new keys that were absent from that constructor row's own catalog. + let selected: Vec<_> = slots.keys().cloned().collect(); + for proposal in &proposals { + if proposal.groups.is_empty() { + continue; + } + let mut proposed = (**base).clone(); + install( + &mut proposed, + proposal.address.target, + proposal.value.clone(), + )?; + let row = state.row(proposal.address).expect("Included row"); + for group in &proposal.groups { + let own = if row.decisions[group] == SemanticDecision::KeepTarget { + &**base + } else { + &proposed + }; + for slot in &selected { + if slot.0 == proposal.address.target && constructors::contains(group, &slot.1) { + agree( + state, + &mut slots, + slot.clone(), + observe(own, slot)?, + proposal.address, + group, + )?; + } + } + } + } + let mut merged = (**base).clone(); + merged.artifact_links.extend(artifact_creations); + let mut groups: Vec<_> = proposals + .iter() + .flat_map(|proposal| proposal.copies.iter().map(move |group| (proposal, group))) + .collect(); + groups.sort_by_key(|(proposal, group)| { + ( + proposal.address.target, + matches!( + group, + FacetSelector::Occurrence(OccurrenceFacet::MessageKind) + ), + ) + }); + for (proposal, group) in groups { + let row = state.row(proposal.address).expect("Included row"); + if row.decisions[group] == SemanticDecision::KeepTarget { + continue; + } + let mut value = record_value(&merged, proposal.address.target)?; + let supplied = &proposal.value; + for path in constructors::roots(base, proposal.address.target, group) + .ok_or("Constructor scope is unavailable.")? + { + *value + .pointer_mut(&path) + .ok_or("Overlapping constructors retire an authored scope.")? = supplied + .pointer(&path) + .cloned() + .ok_or("Constructor candidate is incomplete.")?; + } + install(&mut merged, proposal.address.target, value)?; + } + let mut ordered_slots: Vec<_> = slots.iter().collect(); + ordered_slots.sort_by_key(|((_, facet), _)| matches!(facet, FacetSelector::Order(_))); + for (slot, intention) in ordered_slots { + write_slot(&mut merged, slot, &intention.value)?; + } + let mut final_orders = BTreeMap::new(); + for (target, which, mut order) in implicit_orders { + if slots.contains_key(&(target, FacetSelector::Order(which))) { + continue; + } + let surviving: BTreeSet<_> = ordered::read(&record_value(&merged, target)?, which)? + .into_iter() + .collect(); + order.retain(|id| surviving.contains(id)); + if order.iter().copied().collect::>() != surviving { + return Err("Combined ordered membership needs an explicit final target order. Retained row drafts are unchanged.".into()); + } + if final_orders + .insert((target, which), order.clone()) + .is_some_and(|previous| previous != order) + { + return Err("Contradictory anchored orders need an explicit final target order. Retained row drafts are unchanged.".into()); + } + } + for ((target, which), order) in final_orders { + write_slot( + &mut merged, + &(target, FacetSelector::Order(which)), + &serde_json::json!({"Value":order}), + )?; + } + for (slot, intention) in &slots { + if observe(&merged, slot)? != intention.value { + return Err(format!( + "Combined candidate does not preserve the selected intent for {} · {}.", + base.named(slot.0), + view::title(base, &slot.1) + )); + } + } + for target in candidate_targets { + request + .candidates + .push(semantic_record(&merged, target).ok_or("Merged candidate target disappeared.")?); + } + Ok(request) +} diff --git a/crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs b/crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs new file mode 100644 index 00000000..383f01c7 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/compose_diagnostic_tests.rs @@ -0,0 +1,302 @@ +use super::*; +use serde_json::json; + +fn fixture() -> (Project, RowAddress, Id, Id, KeyPair) { + let mut p = Project::empty(); + let source = p + .add_element(Kind::Activity, Layer::Oa, "Inspect equipment", None) + .unwrap(); + let target = p + .add_element(Kind::Function, Layer::Sa, "Monitor equipment", None) + .unwrap(); + let other = p + .add_element(Kind::Function, Layer::Sa, "Report condition", None) + .unwrap(); + let trace = super::super::tests::add_trace(&mut p, source, target, "Evidence rationale"); + let mut ids = Vec::new(); + for (owner, alias) in [ + (source, "operational_evidence"), + (target, "system_evidence"), + ] { + let id = Id::new_v4(); + p.artifact_links.insert( + id, + ArtifactLink { + id, + alias: alias.into(), + name: "Evidence".into(), + description: String::new(), + owner, + relation: ArtifactRelation::Verifies, + uri: "requirements/interlock".into(), + revision: None, + fragment: None, + }, + ); + ids.push(id); + } + ( + p, + RowAddress { + source, + target, + trace, + }, + ids[1], + other, + KeyPair { + oa: Some(ids[0]), + sa: Some(ids[1]), + }, + ) +} + +fn send(state: &mut State, p: &Project, event: RowEvent) { + let addressed = state.address_event(event); + state.update(p, addressed); +} + +#[test] +fn same_review_presence_and_owner_conflict_names_both_facets_and_owners_and_retains_drafts() { + let (p, row, id, other, keys) = fixture(); + let accepted = p.clone(); + let mut state = State::new(&p, row.trace).unwrap(); + for (layer, key) in [(Layer::Oa, keys.oa), (Layer::Sa, keys.sa)] { + send( + &mut state, + &p, + RowEvent::Artifact(artifacts::Event::Pick( + layer, + key.map(|id| view::choice(&p, id)), + )), + ); + } + send(&mut state, &p, RowEvent::Artifact(artifacts::Event::Stage)); + let presence = artifacts::selector(&keys, ArtifactFacet::Presence); + let owner = artifacts::selector(&keys, ArtifactFacet::Owner); + send( + &mut state, + &p, + RowEvent::Decide(owner.clone(), Some(SemanticDecision::Candidate)), + ); + send( + &mut state, + &p, + RowEvent::Artifact(artifacts::Event::Input( + owner.clone(), + artifacts::Raw::Owner(other.to_string()), + )), + ); + assert!(state.can_apply(), "{:?}", state.error); + send( + &mut state, + &p, + RowEvent::Decide(presence.clone(), Some(SemanticDecision::KeepTarget)), + ); + assert!(!state.can_apply()); + assert!(state.command(&p).is_err()); + let error = state.error.as_deref().unwrap(); + assert!( + error.starts_with("Decisions in this review disagree"), + "{error}" + ); + for expected in [ + "Artifact Presence", + "Artifact Owner", + "Monitor equipment", + "Report condition", + "Evidence rationale", + "@system_evidence", + "@operational_evidence", + "Both drafts are retained", + ] { + assert!(error.contains(expected), "missing {expected}: {error}"); + } + for misleading in ["Independent row", "Absent", "{\"Value\"", "exclude a row"] { + assert!(!error.contains(misleading), "{error}"); + } + assert!( + !error.contains(&row.target.to_string()), + "named owner should not be raw UUID: {error}" + ); + assert_eq!(state.decisions[&presence], SemanticDecision::KeepTarget); + assert_eq!( + state.artifacts.drafts[&owner], + artifacts::Raw::Owner(other.to_string()) + ); + send(&mut state, &p, RowEvent::Decide(presence, None)); + let command = state.command(&p).unwrap(); + let prepared = prepare_command(&p, &command).unwrap().project; + assert_eq!(prepared.artifact_links[&id].owner, other); + assert_eq!(p, accepted); +} + +#[test] +fn distinct_review_conflict_keeps_trace_aliases_even_when_names_are_equal() { + let (mut p, first, id, other, keys) = fixture(); + let trace = + super::super::tests::add_trace(&mut p, first.source, first.target, "Evidence rationale"); + let second = RowAddress { trace, ..first }; + let slot = artifacts::slot(id, ArtifactFacet::Owner); + let presence = artifacts::selector(&keys, ArtifactFacet::Presence); + let owner = artifacts::selector(&keys, ArtifactFacet::Owner); + let mut slots = BTreeMap::new(); + let state = State::new(&p, first.trace).unwrap(); + agree( + &state, + &mut slots, + slot.clone(), + json!({"Value": first.target}), + first, + &presence, + ) + .unwrap(); + let error = agree( + &state, + &mut slots, + slot, + json!({"Value": other}), + second, + &owner, + ) + .unwrap_err(); + assert!( + error.starts_with("Independent row proposals disagree"), + "{error}" + ); + assert!(error.contains(&p.traces[&first.trace].alias), "{error}"); + assert!(error.contains(&p.traces[&second.trace].alias), "{error}"); + assert!( + error.contains("Artifact Presence") && error.contains("Artifact Owner"), + "{error}" + ); + assert!(error.contains("exclude a row"), "{error}"); + assert_eq!(slots.len(), 1); + assert_eq!( + slots.values().next().unwrap().value, + json!({"Value": first.target}) + ); +} + +#[test] +fn reserved_creation_conflict_uses_its_rows_seed_instead_of_a_deleted_reference() { + for reverse in [false, true] { + let (p, row, _, _, existing) = fixture(); + let accepted = p.clone(); + let mut state = State::new(&p, row.trace).unwrap(); + if reverse { + state.update(&p, Event::Direction(TransitionDirection::SaToOa)); + } + let source_layer = if reverse { Layer::Sa } else { Layer::Oa }; + let source = if reverse { + existing.sa.unwrap() + } else { + existing.oa.unwrap() + }; + send( + &mut state, + &p, + RowEvent::Artifact(artifacts::Event::Pick( + source_layer, + Some(view::choice(&p, source)), + )), + ); + send( + &mut state, + &p, + RowEvent::Artifact(artifacts::Event::Reserve), + ); + send(&mut state, &p, RowEvent::Artifact(artifacts::Event::Stage)); + let keys = state.artifacts.pair(); + let destination = if reverse { + keys.oa.unwrap() + } else { + keys.sa.unwrap() + }; + let seed = state.artifacts.seeds[&destination].clone(); + let presence = artifacts::selector(&keys, ArtifactFacet::Presence); + let owner = artifacts::selector(&keys, ArtifactFacet::Owner); + send( + &mut state, + &p, + RowEvent::Decide(presence, Some(SemanticDecision::TakeSource)), + ); + send( + &mut state, + &p, + RowEvent::Decide(owner, Some(SemanticDecision::KeepTarget)), + ); + assert!(!state.can_apply()); + assert!(state.command(&p).is_err()); + let saved = state.recovery_snapshot().unwrap(); + let restored = State::from_recovery(&p, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + for review in [&state, &restored] { + let error = review.error.as_deref().unwrap(); + for expected in [ + "Decisions in this review disagree", + "Artifact Presence", + "Artifact Owner", + "artifact absent", + "Evidence · reserved @", + ] { + assert!(error.contains(expected), "missing {expected}: {error}"); + } + assert!(error.contains(&seed.alias), "{error}"); + for misleading in ["Missing identity", "Deleted reference", "{\"Value\""] { + assert!(!error.contains(misleading), "{error}"); + } + let actual = review.active_address.unwrap(); + assert!(review_identity(review, actual, destination).contains(&seed.alias)); + // A resolver for a different row must not borrow the active row's seed. + let unrelated = RowAddress { + trace: Id::new_v4(), + ..actual + }; + assert_eq!( + review_identity(review, unrelated, destination), + format!("Missing identity {destination}") + ); + } + assert_eq!(p, accepted); + assert!(!p.contains(destination)); + } +} + +#[test] +fn typed_owner_display_does_not_reinterpret_uuid_shaped_artifact_text() { + let (p, row, id, _, _) = fixture(); + let value = json!({"Value": row.target.to_string()}); + assert_eq!( + describe(&p, &artifacts::slot(id, ArtifactFacet::Uri), &value), + json!(row.target.to_string()).to_string() + ); + assert!( + describe(&p, &artifacts::slot(id, ArtifactFacet::Owner), &value) + .contains("owner Monitor equipment") + ); + assert_eq!( + describe( + &p, + &artifacts::slot(id, ArtifactFacet::Owner), + &json!("NotApplicable") + ), + "artifact absent" + ); + assert_eq!( + describe( + &p, + &artifacts::slot(id, ArtifactFacet::Revision), + &json!({"Value": null}) + ), + "null" + ); + assert_eq!( + describe( + &p, + &artifacts::slot(id, ArtifactFacet::Revision), + &json!({"Value": ""}) + ), + "\"\"" + ); +} diff --git a/crates/oa-desktop/src/semantic_review/constructor_tests.rs b/crates/oa-desktop/src/semantic_review/constructor_tests.rs index 22082867..3cc4146d 100644 --- a/crates/oa-desktop/src/semantic_review/constructor_tests.rs +++ b/crates/oa-desktop/src/semantic_review/constructor_tests.rs @@ -165,9 +165,9 @@ fn data_group_candidates_cover_every_definition_and_companion_default_both_direc let paths = constructors::roots(app.project(), target, &group).unwrap(); send( &mut app, - Event::Variant(paths[0].clone(), json!(definition)), + RowEvent::Variant(paths[0].clone(), json!(definition)), ); - send(&mut app, Event::Shape(paths[1].clone(), json!(default))); + send(&mut app, RowEvent::Shape(paths[1].clone(), json!(default))); assert!( state(&app).can_apply(), "{definition:?}: {}", @@ -293,9 +293,9 @@ fn group_take_reference_payload_and_scalar_candidate_match_core_and_one_document assert!(!state(&app).can_apply()); let oa_choice = view::choice(app.project(), refs[0]); let sa_choice = view::choice(app.project(), refs[1]); - send(&mut app, Event::ReferenceOa(oa_choice)); - send(&mut app, Event::ReferenceSa(sa_choice)); - send(&mut app, Event::AddReference); + send(&mut app, RowEvent::ReferenceOa(oa_choice)); + send(&mut app, RowEvent::ReferenceSa(sa_choice)); + send(&mut app, RowEvent::AddReference); assert!( state(&app).can_apply(), "{family} reverse={reverse}: {}", @@ -309,7 +309,7 @@ fn group_take_reference_payload_and_scalar_candidate_match_core_and_one_document ); send( &mut app, - Event::Input("/description".into(), source_ref.to_string()), + RowEvent::Input("/description".into(), source_ref.to_string()), ); assert!( state(&app).can_apply(), @@ -377,14 +377,17 @@ fn data_group_retains_invalid_sibling_drafts_companion_default_and_variant_edito let roots = constructors::roots(app.project(), sa, &group).unwrap(); let min_path = format!("{}/Integer/minimum", roots[0]); decide(&mut app, minimum.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Input(min_path.clone(), "-".into())); + send(&mut app, RowEvent::Input(min_path.clone(), "-".into())); decide(&mut app, minimum.clone(), SemanticDecision::KeepTarget); decide(&mut app, group.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Input(min_path.clone(), "-draft".into())); + send(&mut app, RowEvent::Input(min_path.clone(), "-draft".into())); let cursor = state(&app).group_drafts[&group].inputs[&min_path] .editor .cursor(); - send(&mut app, Event::Variant(roots[0].clone(), json!("Boolean"))); + send( + &mut app, + RowEvent::Variant(roots[0].clone(), json!("Boolean")), + ); assert_eq!( state(&app).group_drafts[&group].value.pointer(&roots[1]), Some(&json!({"Integer":5})) @@ -392,7 +395,7 @@ fn data_group_retains_invalid_sibling_drafts_companion_default_and_variant_edito assert!(!state(&app).can_apply()); // overlap and explicitly incompatible default retained send( &mut app, - Event::Variant( + RowEvent::Variant( roots[0].clone(), json!({"Integer":{"minimum":null,"maximum":null}}), ), @@ -411,16 +414,19 @@ fn data_group_retains_invalid_sibling_drafts_companion_default_and_variant_edito state(&app).draft.as_ref().unwrap().inputs[&min_path].raw, "-" ); - send(&mut app, Event::Shape(min_path.clone(), Value::Null)); - send(&mut app, Event::Variant(roots[0].clone(), json!("Boolean"))); + send(&mut app, RowEvent::Shape(min_path.clone(), Value::Null)); send( &mut app, - Event::Shape(roots[1].clone(), json!({"Boolean":true})), + RowEvent::Variant(roots[0].clone(), json!("Boolean")), ); - send(&mut app, Event::Decide(minimum, None)); + send( + &mut app, + RowEvent::Shape(roots[1].clone(), json!({"Boolean":true})), + ); + send(&mut app, RowEvent::Decide(minimum, None)); assert!(state(&app).can_apply(), "{}", failure(state(&app))); for protected in ["/id", "/alias", "/body", "/parent", "/name"] { - send(&mut app, Event::Shape(protected.into(), Value::Null)); + send(&mut app, RowEvent::Shape(protected.into(), Value::Null)); assert!( state(&app) .error @@ -433,7 +439,7 @@ fn data_group_retains_invalid_sibling_drafts_companion_default_and_variant_edito state(&app).draft.as_ref().unwrap().inputs[&min_path].raw, "-" ); - send(&mut app, Event::Preview); + send(&mut app, RowEvent::Preview); assert!(state(&app).can_apply()); } @@ -595,7 +601,7 @@ fn occurrence_and_event_candidate_variants_cover_closed_families_without_touchin SharedBody::Event { body } => json!(body), _ => unreachable!(), }; - send(&mut app, Event::Shape(root, payload)); + send(&mut app, RowEvent::Shape(root, payload)); assert!( state(&app).can_apply(), "{body:?}: {}", @@ -636,18 +642,18 @@ fn port_group_companions_typed_owner_context_and_direction_restrictions_are_expl let roots = constructors::roots(app.project(), sa, &group).unwrap(); send( &mut app, - Event::Shape(format!("{}/interface", roots[1]), json!(interface)), + RowEvent::Shape(format!("{}/interface", roots[1]), json!(interface)), ); send( &mut app, - Event::Variant( + RowEvent::Variant( roots[0].clone(), json!({"FunctionInput":{"incoming_items":[]}}), ), ); send( &mut app, - Event::Append( + RowEvent::Append( format!("{}/FunctionInput/incoming_items", roots[0]), json!(item), ), @@ -655,7 +661,7 @@ fn port_group_companions_typed_owner_context_and_direction_restrictions_are_expl assert!(state(&app).can_apply(), "{}", failure(state(&app))); send( &mut app, - Event::Variant( + RowEvent::Variant( roots[0].clone(), json!({"FunctionOutput":{"outgoing_items":[]}}), ), @@ -678,11 +684,14 @@ fn port_group_companions_typed_owner_context_and_direction_restrictions_are_expl FacetSelector::Header(HeaderFacet::Parent), SemanticDecision::Candidate, ); - send(&mut app, Event::Shape("/parent".into(), json!(component))); - send(&mut app, Event::Select(group.clone())); send( &mut app, - Event::Variant( + RowEvent::Shape("/parent".into(), json!(component)), + ); + send(&mut app, RowEvent::Select(group.clone())); + send( + &mut app, + RowEvent::Variant( roots[0].clone(), json!({"Component":{"kind":"Flow","orientation":"InOut"}}), ), @@ -693,7 +702,7 @@ fn port_group_companions_typed_owner_context_and_direction_restrictions_are_expl state(&app).group_drafts[&group].context_value()["parent"], json!(component) ); - send(&mut app, Event::Shape(roots[1].clone(), Value::Null)); + send(&mut app, RowEvent::Shape(roots[1].clone(), Value::Null)); assert!(state(&app).can_apply()); apply(&mut app); assert!(matches!( @@ -736,29 +745,29 @@ fn outer_and_nested_constructor_candidates_require_explicit_resolution_and_retai decide(&mut app, nested.clone(), SemanticDecision::Candidate); send( &mut app, - Event::Input(expression.clone(), "nested draft".into()), + RowEvent::Input(expression.clone(), "nested draft".into()), ); decide(&mut app, outer.clone(), SemanticDecision::Candidate); send( &mut app, - Event::Input(expression.clone(), "outer draft".into()), + RowEvent::Input(expression.clone(), "outer draft".into()), ); assert!(!state(&app).can_apply()); assert!(failure(state(&app)).contains("different final values")); send( &mut app, - Event::Variant( + RowEvent::Variant( path.clone(), json!({"Action":{"action":null,"lifeline":null}}), ), ); assert!(!state(&app).can_apply()); assert_eq!(state(&app).decisions.len(), 2); - send(&mut app, Event::Decide(nested.clone(), None)); + send(&mut app, RowEvent::Decide(nested.clone(), None)); assert!(state(&app).can_apply(), "{}", failure(state(&app))); send( &mut app, - Event::Variant( + RowEvent::Variant( path.clone(), json!({"Exchange":{"exchange":null,"sender":null,"receiver":null,"message_kind":"Asynchronous","reply_to":null,"values":[]}}), ), @@ -777,7 +786,7 @@ fn outer_and_nested_constructor_candidates_require_explicit_resolution_and_retai ); send( &mut app, - Event::Variant(path, json!({"Action":{"action":null,"lifeline":null}})), + RowEvent::Variant(path, json!({"Action":{"action":null,"lifeline":null}})), ); assert!(state(&app).can_apply()); let scope = reviewed(state(&app)); @@ -841,7 +850,10 @@ fn historical_event_keep_retires_only_obsolete_branch_and_unchanged_kind_acknowl FacetSelector::Header(HeaderFacet::Name), SemanticDecision::Candidate, ); - send(&mut app, Event::Input("/name".into(), "Kind only".into())); + send( + &mut app, + RowEvent::Input("/name".into(), "Kind only".into()), + ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); assert_eq!( reviewed(state(&app)), @@ -950,7 +962,7 @@ fn existing_fragment_and_branch_group_candidates_edit_complete_owned_order_witho decide(&mut app, group, SemanticDecision::Candidate); send( &mut app, - Event::Array(format!("{root}{suffix}"), Some(1), -1), + RowEvent::Array(format!("{root}{suffix}"), Some(1), -1), ); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let result = state(&app).proposed.as_ref().unwrap(); @@ -1028,7 +1040,7 @@ fn scenario_use_group_takes_typed_binding_references_and_preserves_source_text_b } s.update( &p, - Event::Decide( + RowEvent::Decide( FacetSelector::Occurrence(OccurrenceFacet::Constructor), Some(SemanticDecision::TakeSource), ), @@ -1040,18 +1052,18 @@ fn scenario_use_group_takes_typed_binding_references_and_preserves_source_text_b .clone(); s.update( &p, - Event::Decide( + RowEvent::Decide( FacetSelector::Header(HeaderFacet::Description), Some(SemanticDecision::Candidate), ), ); s.update( &p, - Event::Select(FacetSelector::Header(HeaderFacet::Description)), + RowEvent::Select(FacetSelector::Header(HeaderFacet::Description)), ); s.update( &p, - Event::Input("/description".into(), refs[side][1].to_string()), + RowEvent::Input("/description".into(), refs[side][1].to_string()), ); assert!(s.can_apply(), "{}", failure(&s)); assert_eq!( @@ -1101,7 +1113,7 @@ fn reverse_existing_typed_port_reviews_keep_or_candidate_legacy_without_permitti if decision == SemanticDecision::Candidate { send( &mut app, - Event::Shape("/body/Oa/Shared/Port/legacy/direction".into(), json!("In")), + RowEvent::Shape("/body/Oa/Shared/Port/legacy/direction".into(), json!("In")), ); if let SharedBody::Port { legacy: Some(association), @@ -1174,7 +1186,7 @@ fn historical_same_kind_take_preserves_target_only_literals_when_composed_with_c ); let unit = scalar::binding(app.project(), target, &FacetSelector::Data(DataFacet::Unit)).unwrap(); - send(&mut app, Event::Input(unit, "final unit".into())); + send(&mut app, RowEvent::Input(unit, "final unit".into())); assert!(state(&app).can_apply(), "{}", failure(state(&app))); let SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, @@ -1250,7 +1262,7 @@ fn closed_data_and_event_take_composition_matches_core_across_every_kind_pair_an let mut s = State::new(&p, source).unwrap(); s.update( &p, - Event::Decide(group.clone(), Some(SemanticDecision::TakeSource)), + RowEvent::Decide(group.clone(), Some(SemanticDecision::TakeSource)), ); assert!( s.can_apply(), @@ -1264,18 +1276,18 @@ fn closed_data_and_event_take_composition_matches_core_across_every_kind_pair_an let expected_review = s.plan.as_ref().unwrap().reviews[0].clone(); s.update( &p, - Event::Decide( + RowEvent::Decide( FacetSelector::Header(HeaderFacet::Description), Some(SemanticDecision::Candidate), ), ); s.update( &p, - Event::Select(FacetSelector::Header(HeaderFacet::Description)), + RowEvent::Select(FacetSelector::Header(HeaderFacet::Description)), ); s.update( &p, - Event::Input("/description".into(), "composed candidate".into()), + RowEvent::Input("/description".into(), "composed candidate".into()), ); assert!( s.can_apply(), @@ -1417,7 +1429,7 @@ fn exchange_constructor_takes_qualified_annotation_addresses_without_remapping_e } s.update( &p, - Event::Decide( + RowEvent::Decide( FacetSelector::Occurrence(OccurrenceFacet::Constructor), Some(SemanticDecision::TakeSource), ), @@ -1425,18 +1437,18 @@ fn exchange_constructor_takes_qualified_annotation_addresses_without_remapping_e assert!(s.can_apply(), "reverse={reverse}: {}", failure(&s)); s.update( &p, - Event::Decide( + RowEvent::Decide( FacetSelector::Header(HeaderFacet::Description), Some(SemanticDecision::Candidate), ), ); s.update( &p, - Event::Select(FacetSelector::Header(HeaderFacet::Description)), + RowEvent::Select(FacetSelector::Header(HeaderFacet::Description)), ); s.update( &p, - Event::Input("/description".into(), "annotation reviewed".into()), + RowEvent::Input("/description".into(), "annotation reviewed".into()), ); assert!(s.can_apply(), "reverse={reverse}: {}", failure(&s)); let Some(SharedBody::Occurrence { diff --git a/crates/oa-desktop/src/semantic_review/history.rs b/crates/oa-desktop/src/semantic_review/history.rs index fbedc531..9381fe16 100644 --- a/crates/oa-desktop/src/semantic_review/history.rs +++ b/crates/oa-desktop/src/semantic_review/history.rs @@ -1,10 +1,10 @@ use super::*; -pub(super) fn resettable(facet: &FacetSelector) -> bool { - !matches!(facet, FacetSelector::Artifact { .. }) +pub(super) fn resettable(_facet: &FacetSelector) -> bool { + true } -impl State { +impl ReviewRow { pub(super) fn effective_trace(&self, project: &Project) -> Option { let mut trace = project.traces.get(&self.trace?)?.clone(); if let Some(baseline) = trace.baseline.as_mut().and_then(|b| b.semantic.as_mut()) { @@ -31,7 +31,7 @@ impl State { .and_then(|b| b.semantic.as_ref()) .is_some_and(|b| b.facets.iter().any(|f| f.selector() == facet)); if !exists || !resettable(&facet) { - return Err("Choose an exact existing non-artifact observation to reset.".into()); + return Err("Choose an exact existing observation to reset.".into()); } if reset { self.resets.insert(facet); @@ -41,3 +41,55 @@ impl State { Ok(()) } } + +// Only persisted typed addresses establish correspondence. Order vectors and +// arbitrary scalar values cannot be zipped or searched for UUID-shaped prose. +pub(super) fn reviewed_pairs( + row: &ReviewRow, + project: &Project, +) -> Result, String> { + let trace = row + .effective_trace(project) + .ok_or("Choose an existing trace history.")?; + let mut result = BTreeMap::new(); + for fact in trace + .baseline + .iter() + .flat_map(|b| b.semantic.iter()) + .flat_map(|b| &b.facets) + { + let mut pairs = vec![]; + match fact.selector() { + FacetSelector::Member { keys, .. } + | FacetSelector::Binding(keys) + | FacetSelector::Child(keys) + | FacetSelector::Incident { keys, .. } + | FacetSelector::Artifact { keys, .. } => { + if let (Some(oa), Some(sa)) = (keys.oa, keys.sa) { + pairs.push((oa, sa)); + } + } + FacetSelector::Annotation(KeyPair { + oa: Some(oa), + sa: Some(sa), + }) => { + if oa.path.len() != sa.path.len() { + return Err("Historical annotation addresses have different depths; restore their correspondence explicitly.".into()); + } + pairs.extend( + oa.path + .into_iter() + .chain([oa.field]) + .zip(sa.path.into_iter().chain([sa.field])), + ); + } + _ => {} + } + for (oa, sa) in pairs { + if result.insert(oa, sa).is_some_and(|old| old != sa) { + return Err("Historical observations pair one Operational identity with different System identities. Resolve exact history before restoring pairs.".into()); + } + } + } + Ok(result) +} diff --git a/crates/oa-desktop/src/semantic_review/keyed.rs b/crates/oa-desktop/src/semantic_review/keyed.rs index 0c27ed9e..5b4041c1 100644 --- a/crates/oa-desktop/src/semantic_review/keyed.rs +++ b/crates/oa-desktop/src/semantic_review/keyed.rs @@ -14,6 +14,7 @@ impl std::fmt::Display for Family { Self::Member(ReferenceCollection::FunctionInputItems) => "Function input items", Self::Member(ReferenceCollection::FunctionOutputItems) => "Function output items", Self::Member(ReferenceCollection::SignalItems) => "Signal items", + Self::Member(ReferenceCollection::CategoryExchanges) => "Category exchanges", Self::Member(ReferenceCollection::ExchangeItems) => "Exchange items", Self::Member(ReferenceCollection::TransitionTriggers) => "Transition triggers", Self::Member(ReferenceCollection::TransitionEffects) => "Transition effects", @@ -26,13 +27,14 @@ impl std::fmt::Display for Family { }) } } -pub(super) const FAMILIES: [Family; 12] = [ +pub(super) const FAMILIES: [Family; 13] = [ Family::Member(ReferenceCollection::Steps), Family::Member(ReferenceCollection::FragmentBranches), Family::Member(ReferenceCollection::FunctionInputItems), Family::Member(ReferenceCollection::FunctionOutputItems), Family::Member(ReferenceCollection::SignalItems), Family::Member(ReferenceCollection::ExchangeItems), + Family::Member(ReferenceCollection::CategoryExchanges), Family::Member(ReferenceCollection::TransitionTriggers), Family::Member(ReferenceCollection::TransitionEffects), Family::Member(ReferenceCollection::Constrained), @@ -81,16 +83,22 @@ pub(super) fn path(value: &Value, family: Family) -> Option { Family::Member(ReferenceCollection::FunctionOutputItems) => { &["Port/contract/FunctionOutput/outgoing_items"] } + Family::Member(ReferenceCollection::CategoryExchanges) => &["ExchangeCategory/exchanges"], Family::Member(ReferenceCollection::SignalItems) => &["Event/body/Signal/items"], Family::Member(ReferenceCollection::Constrained) => &["Constraint/constrained"], Family::Literal => &["DataType/definition/Enumeration/literals"], Family::Annotation => &["Occurrence/body/Exchange/values"], Family::Binding => &["Occurrence/body/ScenarioUse/bindings"], Family::Member(ReferenceCollection::ExchangeItems) => { - return ["Interaction", "FunctionalExchange", "BoundaryExchange"] - .iter() - .map(|kind| format!("/body/{kind}/items")) - .find(|p| value.pointer(p).is_some_and(Value::is_array)); + return [ + "Interaction", + "FunctionalExchange", + "BoundaryExchange", + "CommunicationMean", + ] + .iter() + .map(|kind| format!("/body/{kind}/items")) + .find(|p| value.pointer(p).is_some_and(Value::is_array)); } Family::Member(ReferenceCollection::TransitionTriggers) => { return value @@ -164,7 +172,7 @@ pub(super) fn project_presence( Ok(()) } -impl State { +impl ReviewRow { pub(super) fn keyed_value( &self, project: &Project, diff --git a/crates/oa-desktop/src/semantic_review/keyed_rows.rs b/crates/oa-desktop/src/semantic_review/keyed_rows.rs index 54450cc0..1962431a 100644 --- a/crates/oa-desktop/src/semantic_review/keyed_rows.rs +++ b/crates/oa-desktop/src/semantic_review/keyed_rows.rs @@ -37,7 +37,7 @@ pub(super) fn target_selector(facet: &FacetSelector, layer: Layer) -> FacetSelec } } -impl State { +impl ReviewRow { pub(super) fn annotation_address(&self, layer: Layer) -> &AddressDraft { if layer == Layer::Oa { &self.annotation_oa diff --git a/crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs b/crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs index 2d02a748..47c98054 100644 --- a/crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs +++ b/crates/oa-desktop/src/semantic_review/keyed_rows_tests.rs @@ -196,7 +196,7 @@ fn ready(f: &Fixture, reverse: bool) -> State { let mut s = State::new(&f.p, f.owners[usize::from(reverse)]).unwrap(); s.stage_row_references(&f.facet).unwrap(); s.references.insert(f.locals[0], f.locals[1]); - s.update(&f.p, Event::Select(f.facet.clone())); + s.update(&f.p, RowEvent::Select(f.facet.clone())); s } @@ -216,27 +216,27 @@ fn independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_di &capture_facet(&f.p, f.owners[0], f.owners[1], &f.sibling).unwrap(), layer, ); - s.update(&f.p, Event::Decide(f.facet.clone(), Some(decision))); + s.update(&f.p, RowEvent::Decide(f.facet.clone(), Some(decision))); if decision == SemanticDecision::Candidate { - s.update(&f.p, Event::Presence(f.facet.clone(), !binding)); + s.update(&f.p, RowEvent::Presence(f.facet.clone(), !binding)); if !binding { s.update( &f.p, - Event::RowExpression(f.facet.clone(), f.locals[0].to_string()), + RowEvent::RowExpression(f.facet.clone(), f.locals[0].to_string()), ); } } // Force the GUI to compose the row alongside another candidate, // so Take/Keep also exercise adapter scope and mapped references. let header = FacetSelector::Header(HeaderFacet::Description); - s.update(&f.p, Event::Select(header.clone())); + s.update(&f.p, RowEvent::Select(header.clone())); s.update( &f.p, - Event::Decide(header, Some(SemanticDecision::Candidate)), + RowEvent::Decide(header, Some(SemanticDecision::Candidate)), ); s.update( &f.p, - Event::Input("/description".into(), "Reviewed one row".into()), + RowEvent::Input("/description".into(), "Reviewed one row".into()), ); assert!( s.can_apply(), @@ -283,29 +283,29 @@ fn keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_st s.staged.insert(f.facet.clone()); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); if binding { s.update( &f.p, - Event::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])), + RowEvent::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])), ); } else { s.update( &f.p, - Event::RowExpression(f.facet.clone(), "unfinished (\n literal UUID text".into()), + RowEvent::RowExpression(f.facet.clone(), "unfinished (\n literal UUID text".into()), ); } - s.update(&f.p, Event::Presence(f.facet.clone(), false)); + s.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)), ); - s.update(&f.p, Event::DropStaged(f.facet.clone())); - s.update(&f.p, Event::Select(f.facet.clone())); + s.update(&f.p, RowEvent::DropStaged(f.facet.clone())); + s.update(&f.p, RowEvent::Select(f.facet.clone())); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); assert!(!s.presence[&f.facet]); if binding { @@ -323,7 +323,7 @@ fn keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_st // A forged event for an unselected sibling cannot alter its row draft. s.update( &f.p, - Event::RowExpression(f.sibling.clone(), "wrong row".into()), + RowEvent::RowExpression(f.sibling.clone(), "wrong row".into()), ); assert!(s.error.as_ref().unwrap().contains("Select this row")); } @@ -333,21 +333,24 @@ fn keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_st fn annotation_address_browser_descends_qualified_fields_without_changing_rows() { let f = fixture(false); let mut s = ready(&f, false); - s.update(&f.p, Event::MemberFamily(keyed::Family::Annotation)); + s.update(&f.p, RowEvent::MemberFamily(keyed::Family::Annotation)); for layer in [Layer::Oa, Layer::Sa] { let roots = s.annotation_choices(&f.p, layer).unwrap(); assert_eq!(roots.len(), 2); let key = roots[0].key.clone(); - s.update(&f.p, Event::AnnotationBrowse(layer, Some(key.clone()), 0)); + s.update( + &f.p, + RowEvent::AnnotationBrowse(layer, Some(key.clone()), 0), + ); let nested = s.annotation_choices(&f.p, layer).unwrap(); assert_eq!(nested.len(), 1); assert_eq!(nested[0].key.path, vec![key.field]); s.update( &f.p, - Event::AnnotationAddress(layer, Some(nested[0].key.clone())), + RowEvent::AnnotationAddress(layer, Some(nested[0].key.clone())), ); } - s.update(&f.p, Event::StageMember); + s.update(&f.p, RowEvent::StageMember); assert!(s.error.is_none(), "{}", errors(&s)); assert_eq!(s.staged.len(), 1); assert!(s.decisions.is_empty()); @@ -358,7 +361,7 @@ fn annotation_address_browser_descends_qualified_fields_without_changing_rows() fn binding_choices_separate_reused_keys_from_local_lifelines() { let f = fixture(true); let mut s = ready(&f, false); - s.update(&f.p, Event::MemberFamily(keyed::Family::Binding)); + s.update(&f.p, RowEvent::MemberFamily(keyed::Family::Binding)); for (index, layer) in [Layer::Oa, Layer::Sa].into_iter().enumerate() { assert_eq!(s.member_choices(&f.p, layer).len(), 2); assert!( @@ -379,14 +382,14 @@ fn binding_local_picker_follows_a_candidate_parent_before_the_review_is_valid() let local = add(&mut f.p, Kind::Lifeline, Layer::Sa, Some(destination)); let mut s = ready(&f, false); let parent = FacetSelector::Header(HeaderFacet::Parent); - s.update(&f.p, Event::Select(parent.clone())); + s.update(&f.p, RowEvent::Select(parent.clone())); s.update( &f.p, - Event::Decide(parent, Some(SemanticDecision::Candidate)), + RowEvent::Decide(parent, Some(SemanticDecision::Candidate)), ); s.update( &f.p, - Event::Shape("/parent".into(), serde_json::json!(destination)), + RowEvent::Shape("/parent".into(), serde_json::json!(destination)), ); assert_eq!( s.binding_choices(&f.p, Layer::Sa, false) @@ -416,14 +419,14 @@ fn row_review_apply_preserves_exact_source_and_semantic_history_through_undo_red app.document.apply_bundle(bundle).unwrap(); let before = app.document.snapshot(); start(&mut app, f.owners[0]); - send(&mut app, Event::EditPair(f.facet.clone())); - send(&mut app, Event::StageMember); - send(&mut app, Event::Select(f.facet.clone())); + send(&mut app, RowEvent::EditPair(f.facet.clone())); + send(&mut app, RowEvent::StageMember); + send(&mut app, RowEvent::Select(f.facet.clone())); send( &mut app, - Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); - send(&mut app, Event::Presence(f.facet.clone(), false)); + send(&mut app, RowEvent::Presence(f.facet.clone(), false)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let expected = state(&app).proposed.as_ref().unwrap().clone(); apply(&mut app); @@ -503,13 +506,13 @@ fn keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exac let mut s = ready(&f, false); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)), ); assert!( !s.can_apply(), "An old one-sided address cannot silently acquire a paired history" ); - s.update(&f.p, Event::Reset(source_only.clone(), true)); + s.update(&f.p, RowEvent::Reset(source_only.clone(), true)); assert!(s.can_apply(), "{}", errors(&s)); let next = s.proposed.as_ref().unwrap(); let facts = &next.traces[&f.trace] @@ -541,17 +544,17 @@ fn keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exac let mut s = ready(&f, false); s.update( &f.p, - Event::Decide(facet, Some(SemanticDecision::TakeSource)), + RowEvent::Decide(facet, Some(SemanticDecision::TakeSource)), ); let header = FacetSelector::Header(HeaderFacet::Description); s.update( &f.p, - Event::Decide(header.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(header.clone(), Some(SemanticDecision::Candidate)), ); - s.update(&f.p, Event::Select(header)); + s.update(&f.p, RowEvent::Select(header)); s.update( &f.p, - Event::Input("/description".into(), "Compose explicit absence".into()), + RowEvent::Input("/description".into(), "Compose explicit absence".into()), ); assert!(s.can_apply(), "{}", errors(&s)); let fact = capture_facet( @@ -576,18 +579,18 @@ fn binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenar s.references.remove(&f.locals[0]); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::TakeSource)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::TakeSource)), ); assert!(!s.can_apply()); s.references.insert(f.locals[0], f.locals[1]); - s.update(&f.p, Event::Preview); + s.update(&f.p, RowEvent::Preview); assert!(s.can_apply(), "{}", errors(&s)); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); let wrong = s.binding_choices(&f.p, Layer::Sa, true)[0].clone(); - s.update(&f.p, Event::RowLocal(f.facet.clone(), wrong)); + s.update(&f.p, RowEvent::RowLocal(f.facet.clone(), wrong)); assert!(!s.can_apply()); assert!( s.row_drafts[&f.facet].local.is_some(), @@ -595,7 +598,7 @@ fn binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenar ); s.update( &f.p, - Event::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])), + RowEvent::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])), ); assert!(s.can_apply(), "{}", errors(&s)); } @@ -635,16 +638,19 @@ fn keyed_recovery_retains_raw_rows_and_staged_pairs_both_directions_without_appl s.staged.insert(f.facet.clone()); s.update( &f.p, - Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), ); if !binding { s.update( &f.p, - Event::RowExpression(f.facet.clone(), "unfinished (\n exact raw draft".into()), + RowEvent::RowExpression( + f.facet.clone(), + "unfinished (\n exact raw draft".into(), + ), ); } - s.update(&f.p, Event::Presence(f.facet.clone(), false)); - s.update(&f.p, Event::EditPair(f.facet.clone())); + s.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); + s.update(&f.p, RowEvent::EditPair(f.facet.clone())); let saved = s.recovery_snapshot().unwrap(); let restored = State::from_recovery(&f.p, saved).unwrap(); assert_eq!(restored.direction, s.direction); @@ -698,23 +704,23 @@ fn multiple_new_rows_compose_in_empty_collections_using_destination_key_order() let object_count = f.p.elements.len(); let mut s = ready(&f, reverse); for (facet, row) in facets.iter().zip(&original).rev() { - s.update(&f.p, Event::EditPair(facet.clone())); - s.update(&f.p, Event::StageMember); + s.update(&f.p, RowEvent::EditPair(facet.clone())); + s.update(&f.p, RowEvent::StageMember); s.update( &f.p, - Event::Decide(facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)), ); - s.update(&f.p, Event::Presence(facet.clone(), true)); + s.update(&f.p, RowEvent::Presence(facet.clone(), true)); if binding { let local = row["local"].as_str().unwrap().parse().unwrap(); s.update( &f.p, - Event::RowLocal(facet.clone(), view::choice(&f.p, local)), + RowEvent::RowLocal(facet.clone(), view::choice(&f.p, local)), ); } else { s.update( &f.p, - Event::RowExpression(facet.clone(), "new independent value".into()), + RowEvent::RowExpression(facet.clone(), "new independent value".into()), ); } } @@ -734,3 +740,90 @@ fn multiple_new_rows_compose_in_empty_collections_using_destination_key_order() } } } + +#[test] +fn batch_keyed_rows_compare_exact_target_slots_and_retain_independent_values() { + for binding in [false, true] { + let mut f = fixture(binding); + let other = Id::new_v4(); + let mut source = f.p.elements[&f.owners[0]].clone(); + source.id = other; + source.alias = format!("other_{}", other.simple()); + source.name = "Alternate operational rationale".into(); + let parent = source.parent.unwrap(); + f.p.elements.insert(other, source); + f.p.elements + .get_mut(&parent) + .unwrap() + .steps_mut() + .unwrap() + .push(other); + let second = add_trace(&mut f.p, other, f.owners[1], "Alternate keyed rationale"); + validate_candidate(&f.p).unwrap(); + let mut state = ready(&f, false); + state.update( + &f.p, + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + ); + if binding { + state.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); + } else { + state.update( + &f.p, + RowEvent::RowExpression(f.facet.clone(), f.locals[0].to_string()), + ); + } + assert!(state.can_apply(), "{}", errors(&state)); + let first_value = state.proposed.clone().unwrap(); + state.update(&f.p, Event::ChooseTrace(view::choice(&f.p, second))); + state.update(&f.p, Event::OpenTrace); + state.update(&f.p, RowEvent::Select(f.facet.clone())); + state.update( + &f.p, + RowEvent::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)), + ); + if !binding { + state.update( + &f.p, + RowEvent::RowExpression(f.facet.clone(), "Another value".into()), + ); + } + assert!(!state.can_apply()); + assert!(errors(&state).contains("disagree"), "{}", errors(&state)); + let saved = state.recovery_snapshot().unwrap(); + let restored = State::from_recovery(&f.p, saved.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), saved); + assert!(!restored.can_apply()); + if binding { + state.update(&f.p, RowEvent::Presence(f.facet.clone(), false)); + } else { + state.update( + &f.p, + RowEvent::RowExpression(f.facet.clone(), f.locals[0].to_string()), + ); + } + assert!(state.can_apply(), "{}", errors(&state)); + assert_eq!(state.plan.as_ref().unwrap().request.candidates.len(), 1); + assert_eq!( + state.proposed.as_ref().unwrap().elements[&f.owners[1]], + first_value.elements[&f.owners[1]] + ); + assert_eq!( + scalar::value( + &capture_facet( + state.proposed.as_ref().unwrap(), + f.owners[0], + f.owners[1], + &f.sibling + ) + .unwrap(), + Layer::Sa + ), + scalar::value( + &capture_facet(&f.p, f.owners[0], f.owners[1], &f.sibling).unwrap(), + Layer::Sa + ) + ); + let _ = state.view(&f.p); + } +} diff --git a/crates/oa-desktop/src/semantic_review/keyed_tests.rs b/crates/oa-desktop/src/semantic_review/keyed_tests.rs index 9505696c..c28714f8 100644 --- a/crates/oa-desktop/src/semantic_review/keyed_tests.rs +++ b/crates/oa-desktop/src/semantic_review/keyed_tests.rs @@ -194,8 +194,8 @@ fn member_pair(collection: ReferenceCollection) -> (Project, Id, Id, Id, FacetSe ) } fn stage(app: &mut App, facet: &FacetSelector) { - send(app, Event::EditPair(facet.clone())); - send(app, Event::StageMember); + send(app, RowEvent::EditPair(facet.clone())); + send(app, RowEvent::StageMember); assert_eq!( state(app).selected.as_ref(), Some(facet), @@ -240,7 +240,7 @@ fn unordered_members_keep_take_candidate_preserve_other_rows_both_directions() { stage(&mut app, &facet); decide(&mut app, facet.clone(), decision); if decision == SemanticDecision::Candidate { - send(&mut app, Event::Presence(facet.clone(), true)); + send(&mut app, RowEvent::Presence(facet.clone(), true)); } // A target-only Take is an explicit acknowledgment, never implicit removal. decide(&mut app, local_facet.clone(), SemanticDecision::TakeSource); @@ -295,16 +295,16 @@ fn function_port_members_use_existing_typed_choices_and_preserve_oa_legacy() { state(&app).member_families(app.project()), vec![keyed::Family::Member(collection)] ); - send(&mut app, Event::EditPair(facet.clone())); + send(&mut app, RowEvent::EditPair(facet.clone())); assert!( state(&app) .member_choices(app.project(), Layer::Sa) .iter() .any(|c| Some(c.id) == keys.sa) ); - send(&mut app, Event::StageMember); + send(&mut app, RowEvent::StageMember); decide(&mut app, facet.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(facet.clone(), true)); + send(&mut app, RowEvent::Presence(facet.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); apply(&mut app); assert!(contains(app.project(), sa, &facet, oa, sa)); @@ -320,7 +320,7 @@ fn function_port_members_use_existing_typed_choices_and_preserve_oa_legacy() { decide(&mut app, facet.clone(), SemanticDecision::KeepTarget); assert!(state(&app).can_apply(), "{}", errors(state(&app))); decide(&mut app, facet.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(facet, true)); + send(&mut app, RowEvent::Presence(facet, true)); assert!( !state(&app).can_apply(), "OA typed membership must remain inapplicable" @@ -348,7 +348,10 @@ fn exact_literal_new_rows_and_removals_commit_both_directions_without_default_lo stage(&mut app, facet); decide(&mut app, facet.clone(), decision); if decision == SemanticDecision::Candidate { - send(&mut app, Event::Presence(facet.clone(), facet == &created)); + send( + &mut app, + RowEvent::Presence(facet.clone(), facet == &created), + ); } } assert!(state(&app).can_apply(), "{}", errors(state(&app))); @@ -387,7 +390,7 @@ fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directi let before = app.document.snapshot(); let revision = app.document.revision(); start(&mut app, if reverse { sa } else { oa }); - send(&mut app, Event::Reset(reset.clone(), true)); + send(&mut app, RowEvent::Reset(reset.clone(), true)); assert!(state(&app).decisions.is_empty()); assert!(state(&app).can_apply(), "{}", errors(state(&app))); assert_eq!( @@ -399,7 +402,7 @@ fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directi .drift, Drift::Unreviewed ); - send(&mut app, Event::Reset(reset.clone(), false)); + send(&mut app, RowEvent::Reset(reset.clone(), false)); assert!(!state(&app).can_apply()); assert_eq!( state(&app) @@ -410,7 +413,7 @@ fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directi .drift, Drift::Unchanged ); - send(&mut app, Event::Reset(reset, true)); + send(&mut app, RowEvent::Reset(reset, true)); apply(&mut app); assert_eq!(app.document.revision(), revision + 1); assert_eq!(app.project().elements, before.decoded.project.elements); @@ -462,17 +465,17 @@ fn historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap decide(&mut app, previous.clone(), SemanticDecision::KeepTarget); stage(&mut app, &paired); decide(&mut app, paired.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(paired.clone(), true)); + send(&mut app, RowEvent::Presence(paired.clone(), true)); assert!(!state(&app).can_apply()); assert_eq!(state(&app).decisions.len(), 2); - send(&mut app, Event::Decide(previous.clone(), None)); + send(&mut app, RowEvent::Decide(previous.clone(), None)); assert!( !state(&app).can_apply(), "Historic pair cannot silently migrate" ); - send(&mut app, Event::Reset(previous.clone(), true)); + send(&mut app, RowEvent::Reset(previous.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); - send(&mut app, Event::Reset(previous, false)); + send(&mut app, RowEvent::Reset(previous, false)); assert!(!state(&app).can_apply()); assert!(state(&app).presence[&paired]); let old = state(&app).resets.clone(); @@ -486,7 +489,7 @@ fn historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap .unwrap() .facets[0] .selector(); - send(&mut app, Event::Reset(historic, true)); + send(&mut app, RowEvent::Reset(historic, true)); apply(&mut app); assert!(contains(app.project(), target, &paired, oa, sa)); assert_eq!( @@ -515,8 +518,8 @@ fn reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_ let created = literal("separate"); stage(&mut app, &created); decide(&mut app, created.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(created.clone(), true)); - send(&mut app, Event::Reset(ctor.clone(), true)); + send(&mut app, RowEvent::Presence(created.clone(), true)); + send(&mut app, RowEvent::Reset(ctor.clone(), true)); decide(&mut app, ctor.clone(), SemanticDecision::Candidate); assert!(state(&app).can_acknowledge_constructor(app.project(), &ctor)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); @@ -530,10 +533,10 @@ fn reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_ BTreeSet::from([ctor.clone(), created.clone()]) ); assert!(reviewed.retired.is_empty()); - send(&mut app, Event::Reset(ctor.clone(), false)); + send(&mut app, RowEvent::Reset(ctor.clone(), false)); assert!(!state(&app).can_apply()); assert!(state(&app).presence[&created]); - send(&mut app, Event::Reset(ctor, true)); + send(&mut app, RowEvent::Reset(ctor, true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); } } @@ -547,7 +550,7 @@ fn invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repair decide(&mut app, unit.clone(), SemanticDecision::Candidate); send( &mut app, - Event::Input( + RowEvent::Input( "/body/Sa/Shared/DataType/unit".into(), " untouched unit ".into(), ), @@ -555,13 +558,13 @@ fn invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repair decide(&mut app, unit.clone(), SemanticDecision::KeepTarget); let retained = literal("retained"); decide(&mut app, retained.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(retained.clone(), false)); + send(&mut app, RowEvent::Presence(retained.clone(), false)); assert!( !state(&app).can_apply(), "Existing default must prevent removal" ); assert!(!state(&app).presence[&retained]); - send(&mut app, Event::Presence(retained.clone(), true)); + send(&mut app, RowEvent::Presence(retained.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); assert_eq!( state(&app).draft.as_ref().unwrap().inputs["/body/Sa/Shared/DataType/unit"].raw, @@ -585,15 +588,15 @@ fn unsupported_independent_families_and_missing_identities_do_not_gain_edit_auth }; decide(&mut app, unsupported.clone(), SemanticDecision::Candidate); assert!(!state(&app).decisions.contains_key(&unsupported)); - send(&mut app, Event::MemberFamily(keyed::Family::Literal)); - send(&mut app, Event::Literal("unpaired".into())); - send(&mut app, Event::LiteralSide(Layer::Sa, false)); - send(&mut app, Event::StageMember); + send(&mut app, RowEvent::MemberFamily(keyed::Family::Literal)); + send(&mut app, RowEvent::Literal("unpaired".into())); + send(&mut app, RowEvent::LiteralSide(Layer::Sa, false)); + send(&mut app, RowEvent::StageMember); let facet = state(&app).selected.clone().unwrap(); decide(&mut app, facet.clone(), SemanticDecision::Candidate); assert!(!state(&app).can_apply()); - send(&mut app, Event::LiteralSide(Layer::Oa, false)); - send(&mut app, Event::StageMember); + send(&mut app, RowEvent::LiteralSide(Layer::Oa, false)); + send(&mut app, RowEvent::StageMember); assert!(state(&app).error.is_some()); assert_eq!( state(&app).decisions.get(&facet), @@ -654,7 +657,7 @@ fn stale_or_source_failed_reset_and_membership_apply_retain_exact_choices() { history(&mut p, trace, std::slice::from_ref(&reset)); let mut app = App::with_project(p.clone()); start(&mut app, oa); - send(&mut app, Event::Reset(reset.clone(), true)); + send(&mut app, RowEvent::Reset(reset.clone(), true)); app.document .execute(Command::Rename(oa, "Changed after preview".into())) .unwrap(); @@ -669,11 +672,11 @@ fn stale_or_source_failed_reset_and_membership_apply_retain_exact_choices() { limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::() + 10; app.document = Document::from_bundle(bundle, limits).unwrap(); start(&mut app, oa); - send(&mut app, Event::Reset(reset.clone(), true)); + send(&mut app, RowEvent::Reset(reset.clone(), true)); let added = literal(&" exact long literal ".repeat(100)); stage(&mut app, &added); decide(&mut app, added.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(added.clone(), true)); + send(&mut app, RowEvent::Presence(added.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let before = app.document.snapshot(); apply(&mut app); @@ -729,10 +732,10 @@ fn member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys() { let mut app = App::with_project(p); start(&mut app, oa); for tab in [Tab::History, Tab::Members, Tab::Facts] { - send(&mut app, Event::Tab(tab)); + send(&mut app, RowEvent::Tab(tab)); let _ = state(&app).view(app.project()); } - send(&mut app, Event::Reset(old1.clone(), true)); + send(&mut app, RowEvent::Reset(old1.clone(), true)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); assert!(!state(&app).facts.iter().any(|f| f.selector == old1)); assert!(state(&app).facts.iter().any(|f| f.selector == old2)); @@ -745,12 +748,12 @@ fn staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_pres history(&mut p, trace, std::slice::from_ref(&reset)); let mut app = App::with_project(p.clone()); start(&mut app, oa); - send(&mut app, Event::Reset(reset.clone(), true)); + send(&mut app, RowEvent::Reset(reset.clone(), true)); let added = literal("staged draft"); stage(&mut app, &added); decide(&mut app, added.clone(), SemanticDecision::Candidate); - send(&mut app, Event::Presence(added.clone(), true)); - send(&mut app, Event::DropStaged(added.clone())); + send(&mut app, RowEvent::Presence(added.clone(), true)); + send(&mut app, RowEvent::DropStaged(added.clone())); assert!(state(&app).decisions.is_empty()); assert!(!state(&app).staged.contains(&added)); assert!(state(&app).presence[&added]); @@ -776,7 +779,14 @@ fn staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_pres assert!(!s.can_apply()); s.tab = Tab::History; let _ = s.view(&p); - s.update(&p, Event::Reset(reset, false)); - assert!(s.can_apply(), "{}", errors(s)); + s.update(&p, RowEvent::Reset(reset.clone(), false)); + // The old single-row test implicitly adopted externally changed history on + // refresh. A batch has one frozen accepted base: withdrawing raw reset intent + // survives, while application requires an explicitly reopened review. + assert!(!s.resets.contains(&reset)); + assert!(!s.can_apply()); + assert!(s.error.as_ref().unwrap().contains("stale")); + assert!(s.plan.is_none() && s.proposed.is_none()); assert_eq!(s.decisions.get(&added), Some(&SemanticDecision::Candidate)); + assert!(s.presence[&added]); } diff --git a/crates/oa-desktop/src/semantic_review/message_kind_tests.rs b/crates/oa-desktop/src/semantic_review/message_kind_tests.rs index dffc4261..8664508e 100644 --- a/crates/oa-desktop/src/semantic_review/message_kind_tests.rs +++ b/crates/oa-desktop/src/semantic_review/message_kind_tests.rs @@ -118,7 +118,7 @@ fn message_group_take_full_timer_and_scalar_candidate_are_one_source_history_cha ); send( &mut app, - Event::Input("/name".into(), "Reviewed message".into()), + RowEvent::Input("/name".into(), "Reviewed message".into()), ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( @@ -189,7 +189,7 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex ); send( &mut app, - Event::Input(expression.clone(), "independent scalar timer".into()), + RowEvent::Input(expression.clone(), "independent scalar timer".into()), ); decide( &mut app, @@ -200,11 +200,11 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex assert!(!state(&app).can_apply()); send( &mut app, - Event::Input(expression.clone(), " group β timer\n\t ".into()), + RowEvent::Input(expression.clone(), " group β timer\n\t ".into()), ); send( &mut app, - Event::Text(expression.clone(), text_editor::Action::SelectAll), + RowEvent::Text(expression.clone(), text_editor::Action::SelectAll), ); let cursor = state(&app) .group_drafts @@ -222,11 +222,11 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex .selection(); send( &mut app, - Event::Variant(kind.clone(), json!("Asynchronous")), + RowEvent::Variant(kind.clone(), json!("Asynchronous")), ); send( &mut app, - Event::Variant(kind, json!({"Timer":{"expression":""}})), + RowEvent::Variant(kind, json!({"Timer":{"expression":""}})), ); assert_eq!( state(&app) @@ -265,11 +265,11 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex assert_eq!(app.document.bundle(), &before.bundle); // The visible overlap-resolution actions only change the explicitly chosen // decision. Neither independent candidate buffer is discarded. - send(&mut app, Event::Decide(message_kind::selector(), None)); + send(&mut app, RowEvent::Decide(message_kind::selector(), None)); assert!(state(&app).can_apply()); send( &mut app, - Event::Decide(message_kind::timer_selector(), None), + RowEvent::Decide(message_kind::timer_selector(), None), ); decide( &mut app, @@ -395,7 +395,7 @@ fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_dir message_kind::selector(), SemanticDecision::Candidate, ); - send(&mut app, Event::Variant(path(layer), json!("Reply"))); + send(&mut app, RowEvent::Variant(path(layer), json!("Reply"))); decide( &mut app, FacetSelector::Occurrence(OccurrenceFacet::ReplyTo), @@ -409,19 +409,19 @@ fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_dir ); send( &mut app, - Event::ReferenceOa(Choice { + RowEvent::ReferenceOa(Choice { id: calls[0], label: "OA call".into(), }), ); send( &mut app, - Event::ReferenceSa(Choice { + RowEvent::ReferenceSa(Choice { id: calls[1], label: "SA call".into(), }), ); - send(&mut app, Event::AddReference); + send(&mut app, RowEvent::AddReference); assert!(state(&app).can_apply(), "{}", failures(&app)); assert_eq!( acknowledged(&app), @@ -480,7 +480,7 @@ fn message_group_candidate_closed_kinds_are_core_validated_in_both_directions() ); send( &mut app, - Event::Variant(path(layer), serde_json::to_value(&kind).unwrap()), + RowEvent::Variant(path(layer), serde_json::to_value(&kind).unwrap()), ); assert!(state(&app).can_apply(), "{kind:?}: {}", failures(&app)); assert_eq!( @@ -511,7 +511,7 @@ fn message_group_candidate_closed_kinds_are_core_validated_in_both_directions() ); review.update( &p, - Event::Decide(message_kind::selector(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(message_kind::selector(), Some(SemanticDecision::Candidate)), ); assert!(review.decisions.is_empty()); assert!(!review.can_apply()); @@ -552,7 +552,7 @@ fn message_group_timer_does_not_erase_exchange_without_its_own_selected_decision ); send( &mut app, - Event::Variant(path("Sa"), json!({"Timer":{"expression":"after 2 s"}})), + RowEvent::Variant(path("Sa"), json!({"Timer":{"expression":"after 2 s"}})), ); assert!(!state(&app).can_apply()); assert!( @@ -569,7 +569,7 @@ fn message_group_timer_does_not_erase_exchange_without_its_own_selected_decision ); send( &mut app, - Event::Shape( + RowEvent::Shape( "/body/Sa/Shared/Occurrence/body/Exchange/exchange".into(), Value::Null, ), @@ -606,7 +606,10 @@ fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_t FacetSelector::Header(HeaderFacet::Name), SemanticDecision::Candidate, ); - send(&mut app, Event::Input("/name".into(), "Final name".into())); + send( + &mut app, + RowEvent::Input("/name".into(), "Final name".into()), + ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( message_kind::value(state(&app).proposed.as_ref().unwrap(), sa), @@ -621,7 +624,7 @@ fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_t ); send( &mut app, - Event::Input(expression.clone(), "authored group timer".into()), + RowEvent::Input(expression.clone(), "authored group timer".into()), ); assert!( !state(&app).can_apply(), @@ -634,7 +637,7 @@ fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_t ); send( &mut app, - Event::Input(expression.clone(), "different scalar timer".into()), + RowEvent::Input(expression.clone(), "different scalar timer".into()), ); assert!(!state(&app).can_apply()); assert!( @@ -646,7 +649,7 @@ fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_t ); send( &mut app, - Event::Input(expression, "authored group timer".into()), + RowEvent::Input(expression, "authored group timer".into()), ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert!(acknowledged(&app).contains(&message_kind::timer_selector())); @@ -676,11 +679,11 @@ fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_l ); let expression = format!("{}/Timer/expression", path("Sa")); let raw = "retain exact timer β\n".repeat(40); - send(&mut app, Event::Input(expression.clone(), raw.clone())); + send(&mut app, RowEvent::Input(expression.clone(), raw.clone())); assert!(state(&app).can_apply(), "{:?}", state(&app).error); send( &mut app, - Event::Input("/name".into(), "Unselected protected scope".into()), + RowEvent::Input("/name".into(), "Unselected protected scope".into()), ); assert!(!state(&app).can_apply()); assert!( @@ -690,7 +693,7 @@ fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_l .unwrap() .contains("selected review") ); - send(&mut app, Event::Preview); + send(&mut app, RowEvent::Preview); assert!(state(&app).can_apply()); if !source_failure { app.document diff --git a/crates/oa-desktop/src/semantic_review/ordered.rs b/crates/oa-desktop/src/semantic_review/ordered.rs index 2166d252..2674c335 100644 --- a/crates/oa-desktop/src/semantic_review/ordered.rs +++ b/crates/oa-desktop/src/semantic_review/ordered.rs @@ -26,7 +26,7 @@ fn write(value: &mut Value, which: OrderedCollection, ids: Vec) -> Result<() Ok(()) } -impl State { +impl ReviewRow { pub(super) fn ordered_members( &self, project: &Project, diff --git a/crates/oa-desktop/src/semantic_review/ordered_tests.rs b/crates/oa-desktop/src/semantic_review/ordered_tests.rs index 21c7263a..c075437d 100644 --- a/crates/oa-desktop/src/semantic_review/ordered_tests.rs +++ b/crates/oa-desktop/src/semantic_review/ordered_tests.rs @@ -172,8 +172,8 @@ fn ready(f: &Fixture, reverse: bool) -> State { state } fn decide(state: &mut State, p: &Project, facet: FacetSelector, decision: SemanticDecision) { - state.update(p, Event::Select(facet.clone())); - state.update(p, Event::Decide(facet, Some(decision))); + state.update(p, RowEvent::Select(facet.clone())); + state.update(p, RowEvent::Decide(facet, Some(decision))); } fn errors(s: &State) -> String { s.error @@ -231,7 +231,7 @@ fn ordered_take_uses_unique_anchors_and_matches_core_with_unrelated_candidate_bo decide(&mut s, &f.p, header, SemanticDecision::Candidate); s.update( &f.p, - Event::Input("/description".into(), "Reviewed startup insertion".into()), + RowEvent::Input("/description".into(), "Reviewed startup insertion".into()), ); let ti = 1 - usize::from(reverse); let mut expected = f.p.clone(); @@ -268,7 +268,7 @@ fn ambiguous_order_preserves_local_member_and_requires_explicit_candidate_positi s.order_drafts[&f.which], vec![f.rows[ti][0], f.rows[ti][3], f.rows[ti][1], f.rows[ti][2]] ); - s.update(&f.p, Event::OrderMove(f.which, 3, -1)); + s.update(&f.p, RowEvent::OrderMove(f.which, 3, -1)); let mut expected = f.p.clone(); set_order( &mut expected, @@ -297,7 +297,7 @@ fn ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes() { let mut s = ready(&f, false); s.update( &f.p, - Event::MemberFamily(keyed::Family::Member(ordered::collection(f.which))), + RowEvent::MemberFamily(keyed::Family::Member(ordered::collection(f.which))), ); for (i, layer) in [(0, Layer::Oa), (1, Layer::Sa)] { let choices = s.member_choices(&f.p, layer); @@ -311,13 +311,13 @@ fn ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes() { } s.update( &f.p, - Event::MemberOa(Some(view::choice(&f.p, f.rows[0][2]))), + RowEvent::MemberOa(Some(view::choice(&f.p, f.rows[0][2]))), ); s.update( &f.p, - Event::MemberSa(Some(view::choice(&f.p, f.rows[1][2]))), + RowEvent::MemberSa(Some(view::choice(&f.p, f.rows[1][2]))), ); - s.update(&f.p, Event::StageMember); + s.update(&f.p, RowEvent::StageMember); assert!(s.staged.contains(&member(&f, 2))); assert_eq!(s.references[&f.rows[0][2]], f.rows[1][2]); } @@ -330,20 +330,20 @@ fn raw_order_survives_membership_changes_decisions_and_recovery_until_explicit_r let mut s = ready(&f, reverse); let order = FacetSelector::Order(f.which); decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate); - s.update(&f.p, Event::OrderMove(f.which, 1, -1)); + s.update(&f.p, RowEvent::OrderMove(f.which, 1, -1)); let authored = s.order_drafts[&f.which].clone(); decide(&mut s, &f.p, member(&f, 2), SemanticDecision::Candidate); - s.update(&f.p, Event::Presence(member(&f, 2), true)); + s.update(&f.p, RowEvent::Presence(member(&f, 2), true)); assert!(!s.can_apply()); assert_eq!(s.order_drafts[&f.which], authored); - s.update(&f.p, Event::Decide(order.clone(), None)); + s.update(&f.p, RowEvent::Decide(order.clone(), None)); decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate); let saved = s.recovery_snapshot().unwrap(); let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap(); assert_eq!(restored.order_drafts[&f.which], authored); assert!(!restored.can_apply()); assert_eq!(restored.recovery_snapshot().unwrap(), saved); - restored.update(&f.p, Event::OrderReconcile(f.which)); + restored.update(&f.p, RowEvent::OrderReconcile(f.which)); let ti = 1 - usize::from(reverse); let mut expected = f.p.clone(); let mut final_order = authored; @@ -385,7 +385,7 @@ fn order_take_preserves_destination_only_positions_and_order_alone_cannot_add_me s.refresh(&f.p); assert!(!s.can_apply()); assert!(errors(&s).contains("surviving"), "{}", errors(&s)); - s.update(&f.p, Event::OrderReconcile(f.which)); + s.update(&f.p, RowEvent::OrderReconcile(f.which)); assert!(s.can_apply(), "{}", errors(&s)); } } @@ -406,28 +406,28 @@ fn ordered_review_apply_is_exact_source_history_transaction_with_reopen_and_undo for i in 0..3 { send( &mut app, - Event::ReferenceOa(view::choice(&f.p, f.rows[0][i])), + RowEvent::ReferenceOa(view::choice(&f.p, f.rows[0][i])), ); send( &mut app, - Event::ReferenceSa(view::choice(&f.p, f.rows[1][i])), + RowEvent::ReferenceSa(view::choice(&f.p, f.rows[1][i])), ); - send(&mut app, Event::AddReference); + send(&mut app, RowEvent::AddReference); } - send(&mut app, Event::Select(member(&f, 2))); + send(&mut app, RowEvent::Select(member(&f, 2))); send( &mut app, - Event::Decide(member(&f, 2), Some(SemanticDecision::TakeSource)), + RowEvent::Decide(member(&f, 2), Some(SemanticDecision::TakeSource)), ); - send(&mut app, Event::Select(FacetSelector::Order(f.which))); + send(&mut app, RowEvent::Select(FacetSelector::Order(f.which))); send( &mut app, - Event::Decide( + RowEvent::Decide( FacetSelector::Order(f.which), Some(SemanticDecision::Candidate), ), ); - send(&mut app, Event::OrderMove(f.which, 3, -1)); + send(&mut app, RowEvent::OrderMove(f.which, 3, -1)); assert!(state(&app).can_apply(), "{}", errors(state(&app))); let expected = state(&app).proposed.clone().unwrap(); apply(&mut app); @@ -479,7 +479,7 @@ fn several_incoming_members_follow_source_order_despite_independent_identity_sor ); s.update( &f.p, - Event::Input("/description".into(), "Multiple incoming steps".into()), + RowEvent::Input("/description".into(), "Multiple incoming steps".into()), ); let mut expected = f.p.clone(); set_order( @@ -513,7 +513,7 @@ fn ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_tak let mut s = ready(&f, reverse); decide(&mut s, &f.p, member(&f, 0), decision); if decision == SemanticDecision::Candidate { - s.update(&f.p, Event::Presence(member(&f, 0), false)); + s.update(&f.p, RowEvent::Presence(member(&f, 0), false)); } decide( &mut s, @@ -523,7 +523,7 @@ fn ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_tak ); s.update( &f.p, - Event::Input("/description".into(), "Only selected membership".into()), + RowEvent::Input("/description".into(), "Only selected membership".into()), ); let mut expected = f.p.clone(); if decision != SemanticDecision::KeepTarget { @@ -565,3 +565,58 @@ fn ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_tak } } } + +#[test] +fn historical_pair_restore_uses_exact_member_keys_and_never_zips_order_observations() { + let f = fixture(0, false, true); + let mut state = ready(&f, false); + decide( + &mut state, + &f.p, + member(&f, 2), + SemanticDecision::TakeSource, + ); + decide( + &mut state, + &f.p, + FacetSelector::Order(f.which), + SemanticDecision::Candidate, + ); + state.update(&f.p, RowEvent::OrderMove(f.which, 3, -1)); + assert!(state.can_apply(), "{}", errors(&state)); + let project = state.proposed.clone().unwrap(); + let mut reopened = State::new(&project, f.trace).unwrap(); + let exact = history::reviewed_pairs(&reopened, &project).unwrap(); + assert_eq!(exact, BTreeMap::from([(f.rows[0][2], f.rows[1][2])])); + assert_eq!( + project.traces[&f.trace] + .baseline + .as_ref() + .unwrap() + .semantic + .as_ref() + .unwrap() + .facets + .len(), + 2 + ); + reopened.update(&project, RowEvent::RestoreReviewedReferences); + assert_eq!(reopened.references, exact); + assert!(reopened.decisions.is_empty()); + assert_eq!(reopened.proposed.as_ref(), Some(&project)); + reopened.references.insert(f.rows[0][2], f.rows[1][0]); + let prior = reopened.references.clone(); + reopened.update(&project, RowEvent::RestoreReviewedReferences); + assert!(reopened.error.as_ref().unwrap().contains("conflicts")); + assert_eq!( + reopened.references, prior, + "Restore is atomic on disagreement" + ); + reopened.references.clear(); + reopened.update(&project, RowEvent::Reset(member(&f, 2), true)); + assert!( + history::reviewed_pairs(&reopened, &project) + .unwrap() + .is_empty() + ); +} diff --git a/crates/oa-desktop/src/semantic_review/recovery.rs b/crates/oa-desktop/src/semantic_review/recovery.rs index 181ca272..d449ef16 100644 --- a/crates/oa-desktop/src/semantic_review/recovery.rs +++ b/crates/oa-desktop/src/semantic_review/recovery.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] #[serde(deny_unknown_fields)] -struct Saved { +pub(super) struct Saved { version: u32, base_identity: Id, base_fingerprint: String, @@ -31,11 +31,143 @@ struct Saved { rows: Vec<(FacetSelector, String, Option)>, #[serde(default)] orders: Vec<(OrderedCollection, Vec)>, + #[serde(default)] + attachments: attachments::Saved, + #[serde(default)] + artifacts: artifacts::Saved, reference_oa: Option, reference_sa: Option, reference_search: String, accepted_impacts: Vec, } +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct RowPayload { + anchor: Id, + trace: Option, + direction: TransitionDirection, + selected: Option, + decisions: Vec<(FacetSelector, SemanticDecision)>, + resets: BTreeSet, + staged: BTreeSet, + presence: Vec<(FacetSelector, bool)>, + member_family: keyed::Family, + member_oa: Option, + member_sa: Option, + literal: String, + literal_oa: bool, + literal_sa: bool, + annotation_oa: Option, + annotation_sa: Option, + draft: Option, + group_drafts: Vec<(FacetSelector, RecoveryDraft)>, + rows: Vec<(FacetSelector, String, Option)>, + #[serde(default)] + orders: Vec<(OrderedCollection, Vec)>, + #[serde(default)] + attachments: attachments::Saved, + #[serde(default)] + artifacts: artifacts::Saved, + reference_oa: Option, + reference_sa: Option, + reference_search: String, +} +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct SavedRow { + address: RowAddress, + included: bool, + draft: RowPayload, +} +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct SavedBatch { + version: u32, + base_identity: Id, + base_fingerprint: String, + direction: TransitionDirection, + references: BTreeMap, + accepted_impacts: Vec, + active: Option, + rows: Vec, + pending: Option, + add_trace: Option, +} +impl RowPayload { + fn from_row(row: &ReviewRow) -> Result { + let saved: Saved = + serde_json::from_value(row.recovery_snapshot()?).map_err(|e| e.to_string())?; + Ok(Self { + anchor: saved.anchor, + trace: saved.trace, + direction: saved.direction, + selected: saved.selected, + decisions: saved.decisions, + resets: saved.resets, + staged: saved.staged, + presence: saved.presence, + member_family: saved.member_family, + member_oa: saved.member_oa, + member_sa: saved.member_sa, + literal: saved.literal, + literal_oa: saved.literal_oa, + literal_sa: saved.literal_sa, + annotation_oa: saved.annotation_oa, + annotation_sa: saved.annotation_sa, + draft: saved.draft, + group_drafts: saved.group_drafts, + rows: saved.rows, + orders: saved.orders, + attachments: saved.attachments, + artifacts: saved.artifacts, + reference_oa: saved.reference_oa, + reference_sa: saved.reference_sa, + reference_search: saved.reference_search, + }) + } + fn restore( + self, + project: &Project, + references: &BTreeMap, + ) -> Result { + ReviewRow::restore_saved( + project, + Saved { + version: 1, + base_identity: project.id, + base_fingerprint: String::new(), + references: references.clone(), + accepted_impacts: vec![], + anchor: self.anchor, + trace: self.trace, + direction: self.direction, + selected: self.selected, + decisions: self.decisions, + resets: self.resets, + staged: self.staged, + presence: self.presence, + member_family: self.member_family, + member_oa: self.member_oa, + member_sa: self.member_sa, + literal: self.literal, + literal_oa: self.literal_oa, + literal_sa: self.literal_sa, + annotation_oa: self.annotation_oa, + annotation_sa: self.annotation_sa, + draft: self.draft, + group_drafts: self.group_drafts, + rows: self.rows, + orders: self.orders, + attachments: self.attachments, + artifacts: self.artifacts, + reference_oa: self.reference_oa, + reference_sa: self.reference_sa, + reference_search: self.reference_search, + }, + ) + } +} + fn unique(values: Vec<(K, V)>) -> Result, String> { let mut map = BTreeMap::new(); for (key, value) in values { @@ -46,7 +178,7 @@ fn unique(values: Vec<(K, V)>) -> Result, String> { Ok(map) } -impl State { +impl ReviewRow { pub fn recovery_snapshot(&self) -> Result { let key = |address: &keyed_rows::AddressDraft| { address.selected.as_ref().map(|k| AnnotationKey { @@ -100,6 +232,8 @@ impl State { ) }) .collect(), + attachments: self.attachments.recovery_snapshot(), + artifacts: self.artifacts.recovery_snapshot(), reference_oa: self.reference_oa.as_ref().map(|c| c.id), orders: self .order_drafts @@ -124,6 +258,10 @@ impl State { .into(), ); } + Self::restore_saved(project, saved) + } + + fn restore_saved(project: &Project, saved: Saved) -> Result { let count = [ saved.decisions.len(), saved.references.len(), @@ -153,6 +291,7 @@ impl State { }) { return Err("Semantic recovery trace and selected object disagree.".into()); } + state.base_fingerprint = project_fingerprint(project); state.trace = saved.trace; state.direction = saved.direction; state.load_trace(project); @@ -227,6 +366,17 @@ impl State { )?; state.reference_oa = saved.reference_oa.map(|id| view::choice(project, id)); state.order_drafts = unique(saved.orders)?; + state.attachments = attachments::Editor::from_recovery( + project, + state.direction.target_layer(), + saved.attachments, + )?; + if let Some(trace) = state.trace.and_then(|id| project.traces.get(&id)) { + state.artifacts = + artifacts::Editor::from_recovery(project, trace, state.direction, saved.artifacts)?; + } else if !saved.artifacts.is_empty() { + return Err("Recovered artifact drafts need an exact owner trace.".into()); + } state.reference_sa = saved.reference_sa.map(|id| view::choice(project, id)); state.reference_search = saved.reference_search; state.accepted_impacts = saved.accepted_impacts; @@ -237,6 +387,230 @@ impl State { } } +fn batch_limits(value: &Value) -> Result<(), String> { + let mut pending = vec![value]; + let mut count = 0usize; + let mut bytes = 0usize; + while let Some(value) = pending.pop() { + count = count.saturating_add(1); + match value { + Value::String(text) => bytes = bytes.saturating_add(text.len()), + Value::Array(values) => pending.extend(values), + Value::Object(values) => { + bytes = values + .keys() + .fold(bytes, |total, key| total.saturating_add(key.len())); + pending.extend(values.values()); + } + _ => {} + } + if count > crate::source_workspace::LIMITS.model.max_records + || bytes > crate::source_workspace::LIMITS.model.max_source_bytes + { + return Err("Semantic recovery exceeds the aggregate draft limits.".into()); + } + } + Ok(()) +} + +impl State { + /// A source-5 upgrade may reconstruct the same accepted records with a new + /// schema fingerprint. The caller supplies the independently verified legacy + /// base; every saved row is still decoded and replanned through strict restore. + pub(crate) fn from_recovery_after_source5_upgrade( + project: &Project, + legacy_base: &str, + value: Value, + ) -> Result { + batch_limits(&value)?; + let value = match value.get("version").and_then(Value::as_u64) { + Some(1) => { + let mut saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?; + if saved.base_identity != project.id || saved.base_fingerprint != legacy_base { + return Err( + "Semantic source-5 recovery does not match the verified legacy base." + .into(), + ); + } + saved.base_fingerprint = project_fingerprint(project); + saved.accepted_impacts.clear(); + serde_json::to_value(saved).map_err(|e| e.to_string())? + } + Some(2) => { + let mut saved: SavedBatch = + serde_json::from_value(value).map_err(|e| e.to_string())?; + if saved.base_identity != project.id || saved.base_fingerprint != legacy_base { + return Err( + "Semantic source-5 recovery does not match the verified legacy base." + .into(), + ); + } + saved.base_fingerprint = project_fingerprint(project); + saved.accepted_impacts.clear(); + serde_json::to_value(saved).map_err(|e| e.to_string())? + } + _ => return Err("Unsupported semantic recovery version.".into()), + }; + Self::from_recovery(project, value) + } + + pub fn recovery_snapshot(&self) -> Result { + let rows = self + .order + .iter() + .map(|address| { + Ok(SavedRow { + address: *address, + included: self.included.contains(address), + draft: RowPayload::from_row( + self.row(*address) + .ok_or("Retained review row is missing.")?, + )?, + }) + }) + .collect::, String>>()?; + if rows.len() > batch::MAX_REVIEW_ROWS { + return Err("Semantic recovery exceeds the supported row limit.".into()); + } + let saved = SavedBatch { + version: 2, + base_identity: self.base_identity, + base_fingerprint: self.base_fingerprint.clone(), + direction: self.direction, + references: self.references.clone(), + accepted_impacts: self.accepted_impacts.clone(), + active: self.active_address, + rows, + pending: if self.active_address.is_none() { + Some(RowPayload::from_row(&self.active)?) + } else { + None + }, + add_trace: self.add_trace.as_ref().map(|choice| choice.id), + }; + let value = serde_json::to_value(saved).map_err(|error| error.to_string())?; + batch_limits(&value)?; + Ok(value) + } + pub fn from_recovery(project: &Project, value: Value) -> Result { + batch_limits(&value)?; + if value.get("version").and_then(Value::as_u64) == Some(1) { + let active = ReviewRow::from_recovery(project, value)?; + let address = active + .trace + .and_then(|id| project.traces.get(&id)) + .map(|trace| RowAddress::from_trace(trace, active.direction)); + let mut state = Self { + base: std::sync::Arc::new(project.clone()), + base_identity: project.id, + base_fingerprint: project_fingerprint(project), + direction: active.direction, + references: active.references.clone(), + accepted_impacts: active.accepted_impacts.clone(), + plan: None, + proposed: None, + error: None, + active, + active_address: address, + bank: BTreeMap::new(), + order: address.into_iter().collect(), + included: address.into_iter().collect(), + add_trace: None, + notice: None, + }; + state.refresh(project); + return Ok(state); + } + let saved: SavedBatch = serde_json::from_value(value).map_err(|error| error.to_string())?; + if saved.version != 2 + || saved.base_identity != project.id + || saved.base_fingerprint != project_fingerprint(project) + { + return Err( + "Semantic review recovery does not match the accepted model identity and revision." + .into(), + ); + } + if saved.rows.len() > batch::MAX_REVIEW_ROWS { + return Err("Semantic recovery exceeds the supported row limit.".into()); + } + let mut bank = BTreeMap::new(); + let mut order = vec![]; + let mut included = BTreeSet::new(); + let mut sources = BTreeSet::new(); + for saved_row in saved.rows { + let address = saved_row.address; + let direction = address.direction(project)?; + let row = saved_row.draft.restore(project, &saved.references)?; + if row.direction != direction + || row.trace != Some(address.trace) + || row.target(project) != Some(address.target) + { + return Err( + "Recovered row draft does not match its stable source/target/trace address." + .into(), + ); + } + if bank.insert(address, row).is_some() { + return Err("Recovery contains a duplicate semantic row address.".into()); + } + order.push(address); + if saved_row.included { + if !sources.insert(address.source) { + return Err( + "Recovery contains multiple included histories for one source.".into(), + ); + } + included.insert(address); + } + } + let active = match (saved.active, saved.pending) { + (Some(address), None) => { + if address.direction(project)? != saved.direction { + return Err("Recovery active row has a different direction.".into()); + } + bank.remove(&address) + .ok_or("Recovery active review row is absent.")? + } + (None, Some(pending)) if bank.is_empty() => { + let row = pending.restore(project, &saved.references)?; + if row.trace.is_some() || row.direction != saved.direction { + return Err("Recovery pending row is not an unresolved trace selection.".into()); + } + row + } + _ => return Err("Recovery has an invalid active/pending review row.".into()), + }; + if saved + .add_trace + .is_some_and(|id| !project.traces.contains_key(&id)) + { + return Err("Recovery add-row selection is not an existing trace.".into()); + } + let mut state = Self { + base: std::sync::Arc::new(project.clone()), + base_identity: saved.base_identity, + base_fingerprint: saved.base_fingerprint, + direction: saved.direction, + references: saved.references, + accepted_impacts: saved.accepted_impacts, + plan: None, + proposed: None, + error: None, + active, + active_address: saved.active, + bank, + order, + included, + add_trace: saved.add_trace.map(|id| view::choice(project, id)), + notice: None, + }; + // Recompose all included rows once. Excluded invalid raw drafts remain recoverable. + state.refresh(project); + Ok(state) + } +} + #[cfg(test)] mod tests { use super::*; @@ -265,14 +639,14 @@ mod tests { let path = "/body/Sa/Shared/DataType/definition/Integer/minimum"; for (raw, valid) in [("7", true), ("-", false)] { let mut state = State::new(&p, oa).unwrap(); - state.update(&p, Event::Select(facet.clone())); + state.update(&p, RowEvent::Select(facet.clone())); state.update( &p, - Event::Decide(facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)), ); - state.update(&p, Event::Input(path.into(), raw.into())); + state.update(&p, RowEvent::Input(path.into(), raw.into())); assert_eq!(state.can_apply(), valid); - let current = state.recovery_snapshot().unwrap(); + let current = state.active.recovery_snapshot().unwrap(); let mut old = current.clone(); assert_eq!(old["version"], 1); assert_eq!( @@ -283,7 +657,7 @@ mod tests { assert!(restored.order_drafts.is_empty()); assert_eq!(restored.draft.as_ref().unwrap().inputs[path].raw, raw); assert_eq!(restored.can_apply(), valid); - assert_eq!(restored.recovery_snapshot().unwrap(), current); + assert_eq!(restored.active.recovery_snapshot().unwrap(), current); assert_eq!( p, accepted, "Recovery prepares a preview, never an accepted edit" @@ -316,13 +690,13 @@ mod tests { super::super::tests::add_trace(&mut p, oa, sa, "Recover review"); let mut state = State::new(&p, oa).unwrap(); let facet = FacetSelector::Data(DataFacet::IntegerMinimum); - state.update(&p, Event::Select(facet.clone())); + state.update(&p, RowEvent::Select(facet.clone())); state.update( &p, - Event::Decide(facet.clone(), Some(SemanticDecision::Candidate)), + RowEvent::Decide(facet.clone(), Some(SemanticDecision::Candidate)), ); let path = "/body/Sa/Shared/DataType/definition/Integer/minimum"; - state.update(&p, Event::Input(path.into(), "-".into())); + state.update(&p, RowEvent::Input(path.into(), "-".into())); assert!(!state.can_apply()); let saved = state.recovery_snapshot().unwrap(); let restored = State::from_recovery(&p, saved.clone()).unwrap(); diff --git a/crates/oa-desktop/src/semantic_review/row.rs b/crates/oa-desktop/src/semantic_review/row.rs new file mode 100644 index 00000000..8b874549 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/row.rs @@ -0,0 +1,887 @@ +//! Independent authored row; session owns planning and canonical references. +use super::*; + +pub struct ReviewRow { + pub(super) base_identity: Id, + pub(super) base_fingerprint: String, + pub anchor: Id, + pub trace: Option, + pub direction: TransitionDirection, + pub facts: Vec, + pub selected: Option, + pub decisions: BTreeMap, + pub references: BTreeMap, + pub resets: BTreeSet, + pub staged: BTreeSet, + pub presence: BTreeMap, + pub row_drafts: BTreeMap, + pub order_drafts: BTreeMap>, + pub attachments: attachments::Editor, + pub artifacts: artifacts::Editor, + pub annotation_oa: keyed_rows::AddressDraft, + pub annotation_sa: keyed_rows::AddressDraft, + pub member_family: keyed::Family, + pub member_oa: Option, + pub member_sa: Option, + pub literal: String, + pub literal_oa: bool, + pub literal_sa: bool, + pub draft: Option, + // Each closed constructor has an independent draft, including nested kinds. + // Scalar and sibling drafts survive decision and constructor changes. + pub group_drafts: BTreeMap, + pub plan: Option, + pub proposed: Option, + pub error: Option, + pub tab: Tab, + pub show_unchanged: bool, + pub reference_oa: Option, + pub reference_sa: Option, + pub reference_search: String, + pub(super) accepted_impacts: Vec, +} + +impl ReviewRow { + pub fn new(project: &Project, selected: Id) -> Result { + let explicit = project.traces.get(&selected); + let anchor = explicit.map_or(selected, |trace| trace.oa); + let layer = project + .layer_of(anchor) + .ok_or("Select an OA or SA object, relationship or trace to review.")?; + let choices: Vec<_> = project + .traces + .values() + .filter(|t| t.oa == anchor || t.sa == anchor) + .collect(); + if choices.is_empty() { + return Err("This object has no trace. Create a counterpart or link existing counterparts first.".into()); + } + let trace = explicit + .map(|t| t.id) + .or_else(|| (choices.len() == 1).then(|| choices[0].id)); + let mut state = Self { + base_identity: project.id, + base_fingerprint: project_fingerprint(project), + anchor, + trace, + direction: if layer == Layer::Oa { + TransitionDirection::OaToSa + } else { + TransitionDirection::SaToOa + }, + facts: vec![], + selected: None, + decisions: BTreeMap::new(), + references: BTreeMap::new(), + resets: BTreeSet::new(), + staged: BTreeSet::new(), + presence: BTreeMap::new(), + row_drafts: BTreeMap::new(), + order_drafts: BTreeMap::new(), + attachments: Default::default(), + artifacts: Default::default(), + annotation_oa: Default::default(), + annotation_sa: Default::default(), + member_family: keyed::Family::Literal, + member_oa: None, + member_sa: None, + literal: String::new(), + literal_oa: true, + literal_sa: true, + draft: None, + group_drafts: BTreeMap::new(), + plan: None, + proposed: None, + error: None, + tab: Tab::Facts, + show_unchanged: false, + reference_oa: None, + reference_sa: None, + reference_search: String::new(), + accepted_impacts: vec![], + }; + state.load_trace(project); + Ok(state) + } + + pub fn target(&self, project: &Project) -> Option { + let trace = project.traces.get(&self.trace?)?; + Some(if self.direction == TransitionDirection::OaToSa { + trace.sa + } else { + trace.oa + }) + } + + pub(super) fn load_trace(&mut self, project: &Project) { + self.facts.clear(); + self.decisions.clear(); + self.resets.clear(); + self.staged.clear(); + self.presence.clear(); + self.row_drafts.clear(); + self.order_drafts.clear(); + self.attachments = Default::default(); + self.artifacts = Default::default(); + self.annotation_oa = Default::default(); + self.annotation_sa = Default::default(); + self.member_oa = None; + self.member_sa = None; + self.literal.clear(); + self.literal_oa = true; + self.literal_sa = true; + self.selected = None; + self.accepted_impacts.clear(); + self.references.clear(); + self.draft = self + .target(project) + .and_then(|id| Draft::from_project(project, id)); + self.group_drafts.clear(); + if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { + for facet in constructors::selectors() { + if constructors::available(project, trace, &facet) + && let Some(mut draft) = self + .target(project) + .and_then(|id| Draft::from_project(project, id)) + { + draft.semantic_groups = true; + self.group_drafts.insert(facet, draft); + } + } + } + if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { + self.references.insert(trace.oa, trace.sa); + } + self.member_family = self + .member_families(project) + .first() + .copied() + .unwrap_or(keyed::Family::Literal); + self.refresh(project); + } + + pub fn update(&mut self, project: &Project, event: RowEvent) { + let mut replan = true; + let result = match event { + #[cfg(test)] + RowEvent::Trace(choice) => { + if !project + .traces + .get(&choice.id) + .is_some_and(|t| t.oa == self.anchor || t.sa == self.anchor) + { + Err("Choose one of this object's existing traces.".into()) + } else { + self.trace = Some(choice.id); + self.load_trace(project); + return; + } + } + #[cfg(test)] + RowEvent::Direction(direction) => { + self.direction = direction; + self.load_trace(project); + return; + } + RowEvent::Select(facet) => { + self.selected = Some(facet); + self.tab = Tab::Facts; + replan = false; + Ok(()) + } + RowEvent::Decide(facet, decision) => { + if let Some(decision) = decision { + if !self.editable(project, &facet) + && !(decision == SemanticDecision::KeepTarget + && self.can_acknowledge_constructor(project, &facet)) + { + Err("This fact needs the structured review workflow; its history remains unchanged.".into()) + } else { + if artifacts::supported(&facet) + && decision == SemanticDecision::Candidate + && let Err(error) = self.artifacts.ensure( + project, + &facet, + self.direction.target_layer(), + ) + { + self.error = Some(error); + return; + } + if attachments::supported(&facet) + && decision == SemanticDecision::Candidate + && let Err(error) = self.attachments.ensure( + project, + &facet, + self.direction.target_layer(), + ) + { + self.error = Some(error); + return; + } + if let FacetSelector::Order(which) = facet + && decision == SemanticDecision::Candidate + { + if let Err(error) = self.initialize_order(project, which) { + self.error = Some(error); + return; + } + } else if keyed_rows::supported(&facet) { + self.initialize_row(project, &facet); + } else if keyed::supported(&facet) { + let target_presence = self + .effective_trace(project) + .and_then(|t| capture_facet(project, t.oa, t.sa, &facet).ok()) + .and_then(|f| scalar::value(&f, self.direction.target_layer())) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + self.presence + .entry(facet.clone()) + .or_insert(target_presence); + } + self.decisions.insert(facet, decision); + Ok(()) + } + } else { + self.decisions.remove(&facet); + Ok(()) + } + } + RowEvent::Input(path, value) => self.edit(project, &path, |d| { + d.input(&path, value); + Ok(()) + }), + RowEvent::Text(path, action) => self.edit(project, &path, |d| { + d.edit_text(&path, action); + Ok(()) + }), + RowEvent::Shape(path, value) => { + self.edit(project, &path, |d| d.shape_scoped(&path, value)) + } + RowEvent::Variant(path, value) => { + self.edit(project, &path, |d| d.variant_scoped(&path, value)) + } + RowEvent::Array(path, index, direction) => { + self.edit(project, &path, |d| d.array_scoped(&path, index, direction)) + } + RowEvent::Append(path, value) => { + self.edit(project, &path, |d| d.append_scoped(&path, value)) + } + RowEvent::ValuePath(path, key) => { + self.edit(project, &path, |d| d.value_path_scoped(&path, key)) + } + RowEvent::ValueBrowse(path, scope, page) => { + replan = false; + self.edit(project, &path, |draft| { + draft + .value_browsers + .insert(path.clone(), crate::forms::ValueBrowser { scope, page }); + Ok(()) + }) + } + RowEvent::ReferenceBrowse(path, query, page) => { + replan = false; + self.edit(project, &path, |draft| { + let page = if draft + .reference_browsers + .get(&path) + .is_some_and(|b| b.query == query) + { + page + } else { + 0 + }; + draft + .reference_browsers + .insert(path.clone(), crate::forms::ReferenceBrowser { query, page }); + Ok(()) + }) + } + RowEvent::Tab(tab) => { + self.tab = tab; + replan = false; + Ok(()) + } + RowEvent::ShowUnchanged(show) => { + self.show_unchanged = show; + replan = false; + Ok(()) + } + RowEvent::ReferenceOa(choice) => { + self.reference_oa = Some(choice); + replan = false; + Ok(()) + } + RowEvent::ReferenceSa(choice) => { + self.reference_sa = Some(choice); + replan = false; + Ok(()) + } + RowEvent::ReferenceSearch(value) => { + self.reference_search = value; + replan = false; + Ok(()) + } + RowEvent::AddReference => match (&self.reference_oa, &self.reference_sa) { + (Some(oa), Some(sa)) => { + self.references.insert(oa.id, sa.id); + Ok(()) + } + _ => Err("Choose both an Operational and a System reference.".into()), + }, + RowEvent::RestoreReviewedReferences => { + Err("Restore reviewed pairs through the complete review session.".into()) + } + RowEvent::RemoveReference(id) => { + self.references.remove(&id); + Ok(()) + } + RowEvent::AcceptImpact(index, accepted) => { + if let Some(impact) = self + .plan + .as_ref() + .and_then(|p| p.impacts.get(index)) + .cloned() + { + self.accepted_impacts.retain(|i| i.object != impact.object); + if accepted { + self.accepted_impacts.push(impact); + } + } + self.refresh(project); + return; + } + RowEvent::Preview => Ok(()), + RowEvent::OrderMove(which, index, delta) => self.move_order(which, index, delta), + RowEvent::OrderReconcile(which) => self.reconcile_order(project, which), + RowEvent::Reset(facet, reset) => self.reset_history(project, facet, reset), + RowEvent::Presence(facet, present) => { + if keyed::supported(&facet) + && self.decisions.get(&facet) == Some(&SemanticDecision::Candidate) + { + self.presence.insert(facet, present); + Ok(()) + } else { + Err("Choose Set final value for this member before editing presence.".into()) + } + } + RowEvent::MemberFamily(family) => { + self.member_family = family; + replan = false; + Ok(()) + } + RowEvent::MemberOa(choice) => { + self.member_oa = choice; + replan = false; + Ok(()) + } + RowEvent::MemberSa(choice) => { + self.member_sa = choice; + replan = false; + Ok(()) + } + RowEvent::Literal(value) => { + self.literal = value; + replan = false; + Ok(()) + } + RowEvent::LiteralSide(layer, present) => { + if layer == Layer::Oa { + self.literal_oa = present; + } else { + self.literal_sa = present; + } + replan = false; + Ok(()) + } + RowEvent::EditPair(facet) => { + replan = false; + if artifacts::supported(&facet) { + self.artifacts.select_pair(project, &facet).map(|()| { + self.tab = Tab::Artifacts; + }) + } else if attachments::supported(&facet) { + self.attachments.select_pair(project, &facet).map(|()| { + self.tab = Tab::Attachments; + }) + } else { + self.edit_pair(project, facet) + } + } + RowEvent::Artifact(event) => { + replan = !matches!( + event, + artifacts::Event::Pick(_, _) | artifacts::Event::Search(_) + ); + let edited = match &event { + artifacts::Event::Input(facet, _) | artifacts::Event::Description(facet, _) => { + Some(facet) + } + _ => None, + }; + if edited.is_some_and(|facet| { + self.decisions.get(facet) != Some(&SemanticDecision::Candidate) + }) { + Err("Choose Set final value before editing this exact artifact facet.".into()) + } else { + match self.effective_trace(project) { + Some(trace) => self + .artifacts + .update(project, &trace, self.direction, event) + .map(|facets| { + if let Some(facets) = facets { + let pair = self.artifacts.pair(); + if let Some((oa, sa)) = pair.oa.zip(pair.sa) { + self.references.insert(oa, sa); + } + self.selected = facets.first().cloned(); + self.staged.extend(facets); + self.tab = Tab::Facts; + } + }), + None => Err("Choose a trace first.".into()), + } + } + } + RowEvent::Attachment(event) => { + replan = matches!(event, attachments::Event::Stage); + match self.effective_trace(project) { + Some(trace) => self + .attachments + .update(project, &trace, event) + .map(|facet| { + if let Some(facet) = facet { + if let Some((oa, sa)) = attachments::explicit_pair(&facet) { + self.references.insert(oa, sa); + } + self.staged.insert(facet.clone()); + self.selected = Some(facet); + self.tab = Tab::Facts; + } + }), + None => Err("Choose a trace first.".into()), + } + } + RowEvent::StageMember => self.stage_member(project), + RowEvent::DropStaged(facet) => { + if self.staged.remove(&facet) { + self.decisions.remove(&facet); + } + Ok(()) + } + RowEvent::AnnotationAddress(layer, key) => { + self.annotation_address_mut(layer).selected = key; + replan = false; + Ok(()) + } + RowEvent::AnnotationBrowse(layer, scope, page) => { + let address = self.annotation_address_mut(layer); + address.scope = scope; + address.page = page; + replan = false; + Ok(()) + } + #[cfg(test)] + RowEvent::RowExpression(facet, expression) => self.edit_row(&facet, |draft| { + draft.editor = text_editor::Content::with_text(&expression); + draft.expression = expression; + }), + RowEvent::RowText(facet, action) => self.edit_row(&facet, |draft| { + draft.editor.perform(action); + draft.expression = draft.editor.text(); + }), + RowEvent::RowLocal(facet, local) => self.edit_row(&facet, |draft| { + draft.local = Some(local); + }), + }; + if let Err(error) = result { + self.error = Some(error); + self.plan = None; + self.proposed = None; + return; + } + if replan { + self.accepted_impacts.clear(); + self.refresh(project); + } + } + + fn edit( + &mut self, + project: &Project, + path: &str, + action: impl FnOnce(&mut Draft) -> Result<(), String>, + ) -> Result<(), String> { + let facet = self.selected.as_ref().ok_or("Choose a property first.")?; + if self.decisions.get(facet) != Some(&SemanticDecision::Candidate) { + return Err("Choose Set final value before editing this property.".into()); + } + let roots = self + .candidate_roots(project, facet) + .ok_or("Selected property is unavailable.")?; + if !roots + .iter() + .any(|root| path == root || path.starts_with(&format!("{root}/"))) + { + return Err("Only the selected review property can change in this draft.".into()); + } + let draft = if attachments::supported(facet) { + self.attachments.drafts.get_mut(facet) + } else if scalar::grouped(facet) { + self.group_drafts.get_mut(facet) + } else { + self.draft.as_mut() + } + .ok_or("Choose an existing target.")?; + // The scoped view sends only the selected property or closed group roots. + // Protected identity and top-level body/perspective controls are never exposed. + if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" { + return Err("Semantic candidates retain identity, alias and perspective.".into()); + } + action(draft) + } + + pub(super) fn candidate_roots( + &self, + project: &Project, + facet: &FacetSelector, + ) -> Option> { + if attachments::supported(facet) { + attachments::path(project, facet, self.direction.target_layer()).map(|path| vec![path]) + } else { + self.target(project) + .and_then(|id| constructors::roots(project, id, facet)) + } + } + + pub fn editable(&self, project: &Project, facet: &FacetSelector) -> bool { + if artifacts::supported(facet) { + return self.effective_trace(project).is_some_and(|trace| { + self.artifacts + .editable(project, &trace, facet, self.direction.target_layer()) + }); + } + if attachments::supported(facet) { + return attachments::path(project, facet, self.direction.target_layer()).is_some() + && self.effective_trace(project).is_some_and(|trace| { + capture_facet(project, trace.oa, trace.sa, facet).is_ok() + }); + } + if keyed::supported(facet) || matches!(facet, FacetSelector::Order(_)) { + return self + .effective_trace(project) + .is_some_and(|trace| capture_facet(project, trace.oa, trace.sa, facet).is_ok()); + } + self.target(project) + .and_then(|id| scalar::binding(project, id, facet)) + .is_some() + && (!scalar::grouped(facet) + || (self.group_drafts.contains_key(facet) + && self + .trace + .and_then(|id| project.traces.get(&id)) + .is_some_and(|trace| constructors::available(project, trace, facet)))) + } + + pub(super) fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> { + if attachments::supported(facet) { + self.attachments.drafts.get(facet) + } else if scalar::grouped(facet) { + self.group_drafts.get(facet) + } else { + self.draft.as_ref() + } + } + + pub fn can_acknowledge_constructor(&self, project: &Project, facet: &FacetSelector) -> bool { + if !scalar::grouped(facet) { + return false; + } + let Some(trace) = self.effective_trace(project) else { + return false; + }; + if trace + .baseline + .as_ref() + .and_then(|b| b.semantic.as_ref()) + .is_some_and(|b| b.facets.iter().any(|f| f.selector() == *facet)) + { + return false; + } + capture_facet(project, trace.oa, trace.sa, facet) + .ok() + .is_some_and(|current| { + scalar::value(¤t, Layer::Oa).is_some() + && scalar::observation(¤t, Layer::Oa) + == scalar::observation(¤t, Layer::Sa) + }) + } + + /// Compose selected values for both the candidate and dependent form + /// controls. Incomplete inputs retain their stored shape only for controls; + /// request construction always requires every selected input to parse. + pub(super) fn combined_value( + &self, + project: &Project, + form_context: bool, + ) -> Result { + self.combine(project, form_context, false) + } + + pub(super) fn combine( + &self, + project: &Project, + form_context: bool, + skip_orders: bool, + ) -> Result { + let trace = self.effective_trace(project).ok_or("Choose a trace.")?; + let (source, target) = if self.direction == TransitionDirection::OaToSa { + (trace.oa, trace.sa) + } else { + (trace.sa, trace.oa) + }; + let mut final_value = Draft::from_project(project, target) + .ok_or("Target has no editable record.")? + .value; + let mut map = BTreeMap::new(); + for (oa, sa) in &self.references { + let (from, to) = if self.direction == TransitionDirection::OaToSa { + (*oa, *sa) + } else { + (*sa, *oa) + }; + if map.insert(from, to).is_some_and(|old| old != to) { + return Err("Reverse reference correspondence is ambiguous.".into()); + } + } + map.insert(source, target); + let original = final_value.clone(); + let mut group_candidates = vec![]; + // Outer groups precede nested groups and leaves. The core rejects actual + // overlapping replacement scopes; this order also supports first-equal + // discriminator acknowledgment together with explicit descendant values. + let mut decisions: Vec<_> = self.decisions.iter().collect(); + decisions.sort_by_key(|(facet, _)| { + let group = match facet { + FacetSelector::Occurrence(OccurrenceFacet::Constructor) => 0, + facet if scalar::grouped(facet) => 1, + FacetSelector::Order(_) => 3, + _ => 2, + }; + ( + group, + keyed_rows::target_selector(facet, self.direction.target_layer()), + ) + }); + for (facet, decision) in decisions { + if attachments::supported(facet) + || artifacts::supported(facet) + || *decision == SemanticDecision::KeepTarget + { + continue; + } + if let FacetSelector::Order(which) = facet { + if !skip_orders { + // Invalid order drafts remain visible while dependent controls use the + // surviving membership; request construction still rejects them. + if let Err(error) = + self.project_order(project, &trace, &mut final_value, *which, *decision) + && !form_context + { + return Err(error); + } + } + continue; + } + if matches!(facet, FacetSelector::Member { collection, .. } if ordered::ordered(*collection).is_some()) + { + self.project_ordered_member( + project, + &trace, + &mut final_value, + facet, + *decision, + form_context, + )?; + continue; + } + if keyed::supported(facet) { + self.keyed_value(project, &trace, &mut final_value, facet, *decision)?; + continue; + } + let roots = constructors::roots(project, target, facet) + .ok_or("Selected property is not applicable to the target.")?; + let supplied = match decision { + SemanticDecision::KeepTarget => continue, + SemanticDecision::Candidate => { + let draft = self + .candidate_draft(facet) + .ok_or("Target draft is unavailable.")?; + match draft.materialize_scopes(&roots) { + Ok(value) => value, + Err(_) if form_context => draft.value.clone(), + Err(error) => return Err(error), + } + } + SemanticDecision::TakeSource if scalar::grouped(facet) => { + if self.can_acknowledge_constructor(project, facet) { + original.clone() + } else { + constructors::take_value(project, &trace, self.direction, facet, &map)? + } + } + SemanticDecision::TakeSource => { + let fact = capture_facet(project, trace.oa, trace.sa, facet)?; + let value = scalar::source_value(&fact, self.direction.source_layer(), &map)?; + let mut supplied = original.clone(); + *supplied + .pointer_mut(&roots[0]) + .ok_or("Source property is unavailable.")? = value; + supplied + } + }; + for path in roots { + let value = supplied + .pointer(&path) + .cloned() + .ok_or("Candidate property is no longer available.")?; + if scalar::grouped(facet) && *decision == SemanticDecision::Candidate { + group_candidates.push((path.clone(), value.clone())); + } + *final_value.pointer_mut(&path).ok_or("Final target property is unavailable; resolve the overlapping constructor decision.")? = value; + } + } + if !form_context { + for (path, authored) in group_candidates { + if !original + .pointer(&path) + .zip(final_value.pointer(&path)) + .is_some_and(|(original, final_value)| { + constructors::authored_agrees(original, &authored, final_value) + }) + { + return Err("The constructor candidate and descendant decision propose different final values. Keep the kind or make their final values agree; both drafts are retained.".into()); + } + } + } + Ok(final_value) + } + + pub fn refresh(&mut self, project: &Project) { + self.plan = None; + self.proposed = None; + self.error = None; + let context = self.combined_value(project, true).ok(); + if let Some(draft) = &mut self.draft { + draft.set_context(context.clone()); + } + for draft in self.group_drafts.values_mut() { + draft.set_context(context.clone()); + } + let Some(trace) = self.effective_trace(project) else { + self.error = Some( + "Several traces are available. Choose the correspondence and review history above." + .into(), + ); + return; + }; + match semantic_facets(project, &trace, &self.references) { + Ok(mut facts) => { + // Explicit addresses survive catalog normalization, pairing changes and resets. + for selector in self.staged.iter().chain(self.decisions.keys()) { + if !facts.iter().any(|f| &f.selector == selector) { + match capture_facet(project, trace.oa, trace.sa, selector) { + Ok(current) => { + let previous = trace + .baseline + .as_ref() + .and_then(|b| b.semantic.as_ref()) + .and_then(|b| { + b.facets.iter().find(|f| f.selector() == *selector) + }); + facts.push(FacetDrift { + selector: selector.clone(), + drift: facet_drift(¤t, previous), + current, + }); + } + Err(error) => { + self.error = Some(error); + return; + } + } + } + } + facts.sort_by(|a, b| a.selector.cmp(&b.selector)); + self.facts = facts; + } + Err(error) => { + self.error = Some(error); + return; + } + } + if self + .selected + .as_ref() + .is_none_or(|s| !self.facts.iter().any(|f| &f.selector == s)) + { + self.selected = self + .facts + .iter() + .find(|f| f.drift != Drift::Unchanged) + .or(self.facts.first()) + .map(|f| f.selector.clone()); + } + } + + pub fn all_references(&self, project: &Project, layer: Layer) -> Vec { + let historical: BTreeSet<_> = self + .facts + .iter() + .flat_map(|f| scalar::references(&f.current)) + .chain( + self.trace + .and_then(|id| project.traces.get(&id)) + .and_then(|t| t.baseline.as_ref()) + .and_then(|b| b.semantic.as_ref()) + .into_iter() + .flat_map(|b| b.facets.iter().flat_map(scalar::references)), + ) + .collect(); + let mut ids: BTreeSet<_> = project + .elements + .keys() + .chain(project.relations.keys()) + .chain(project.artifact_links.keys()) + .filter(|id| project.layer_of(**id) == Some(layer)) + .copied() + .collect(); + ids.extend(historical.into_iter().filter(|id| !project.contains(*id))); + let search = self.reference_search.to_lowercase(); + let mut choices: Vec<_> = ids + .into_iter() + .map(|id| self.review_choice(project, id)) + .filter(|c| search.is_empty() || c.label.to_lowercase().contains(&search)) + .collect(); + choices.sort_by(|a, b| a.label.cmp(&b.label)); + choices + } +} + +pub fn form_event(message: Message) -> Message { + let event = match message { + Message::Field(path, value) => RowEvent::Input(path, value), + Message::FieldText(path, action) => RowEvent::Text(path, action), + Message::FieldShape(path, value) => RowEvent::Shape(path, value), + Message::FieldVariant(path, value) => RowEvent::Variant(path, value), + Message::FieldArray(path, index, direction) => RowEvent::Array(path, index, direction), + Message::FieldAppend(path, value) => RowEvent::Append(path, value), + Message::FieldValuePath(path, key) => RowEvent::ValuePath(path, key), + Message::FieldValueBrowse(path, scope, page) => RowEvent::ValueBrowse(path, scope, page), + Message::FieldReferenceBrowse(path, query, page) => { + RowEvent::ReferenceBrowse(path, query, page) + } + _ => RowEvent::Preview, + }; + Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event))) +} diff --git a/crates/oa-desktop/src/semantic_review/scalar.rs b/crates/oa-desktop/src/semantic_review/scalar.rs index 321cf724..a35b01af 100644 --- a/crates/oa-desktop/src/semantic_review/scalar.rs +++ b/crates/oa-desktop/src/semantic_review/scalar.rs @@ -61,6 +61,7 @@ fn path(own: &Own, facet: &FacetSelector) -> Option> { F::ExchangeItem(ExchangeItemFacet::Nature) => ("ExchangeItem", &["kind"]), F::ExchangeItem(ExchangeItemFacet::Mechanism) => ("ExchangeItem", &["mechanism"]), F::ExchangeItem(ExchangeItemFacet::Supertype) => ("ExchangeItem", &["supertype"]), + F::Category(CategoryFacet::Family) => ("ExchangeCategory", &["family"]), F::Interface(InterfaceFacet::Supertype) => ("Interface", &["supertype"]), F::Data(DataFacet::Constructor) => ("DataType", &["definition"]), F::Data(DataFacet::IntegerMinimum) => ("DataType", &["definition", "Integer", "minimum"]), @@ -156,7 +157,14 @@ fn path(own: &Own, facet: &FacetSelector) -> Option> { F::Relation(RelationFacet::TargetNavigable) => ("DataAssociation", &["target_navigable"]), F::Relation(RelationFacet::SourcePort) => ("FunctionalExchange", &["source_port"]), F::Relation(RelationFacet::TargetPort) => ("FunctionalExchange", &["target_port"]), - F::Relation(RelationFacet::Oriented) => ("BoundaryExchange", &["oriented"]), + F::Relation(RelationFacet::Oriented) => ( + if own.kind == "CommunicationMean" { + "CommunicationMean" + } else { + "BoundaryExchange" + }, + &["oriented"], + ), F::Relation(RelationFacet::ExtendCondition) => ("CapabilityExtend", &["condition"]), F::Relation(RelationFacet::SequenceGuard) => ("SequenceLink", &["guard"]), F::Relation(RelationFacet::TransitionKind) => ("StateTransition", &["transition_kind"]), diff --git a/crates/oa-desktop/src/semantic_review/tests.rs b/crates/oa-desktop/src/semantic_review/tests.rs index 25e3544d..ed28d9f8 100644 --- a/crates/oa-desktop/src/semantic_review/tests.rs +++ b/crates/oa-desktop/src/semantic_review/tests.rs @@ -2,8 +2,10 @@ use super::*; use crate::{app::App, document::Document, workflow::Workflow}; use serde_json::json; -pub(super) fn send(app: &mut App, event: Event) { - let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event))); +pub(super) fn send(app: &mut App, event: RowEvent) { + let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic( + state(app).address_event(event), + ))); } pub(super) fn apply(app: &mut App) { let _ = app.update(Message::Workflow(crate::workflow::Event::Apply)); @@ -72,7 +74,7 @@ fn independent_review_clear_bound_ignores_retained_invalid_other_facet() { facet(DataFacet::IntegerMinimum), SemanticDecision::Candidate, ); - send(&mut app, Event::Input(minimum.into(), "-".into())); + send(&mut app, RowEvent::Input(minimum.into(), "-".into())); decide( &mut app, facet(DataFacet::IntegerMinimum), @@ -85,7 +87,7 @@ fn independent_review_clear_bound_ignores_retained_invalid_other_facet() { ); assert!(state(&app).can_apply()); // Exact event emitted by the upper bound's visible Clear bound button. - send(&mut app, Event::Shape(maximum.into(), Value::Null)); + send(&mut app, RowEvent::Shape(maximum.into(), Value::Null)); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!(state(&app).draft.as_ref().unwrap().inputs[minimum].raw, "-"); assert!( @@ -100,15 +102,15 @@ fn independent_review_clear_bound_ignores_retained_invalid_other_facet() { ); // Clear must also discard the selected invalid value, without requiring it // to parse before removal. A valid but uncommitted neighboring input survives. - send(&mut app, Event::Shape(maximum.into(), json!(30))); - send(&mut app, Event::Input(maximum.into(), "29".into())); + send(&mut app, RowEvent::Shape(maximum.into(), json!(30))); + send(&mut app, RowEvent::Input(maximum.into(), "29".into())); decide( &mut app, facet(DataFacet::IntegerMinimum), SemanticDecision::Candidate, ); assert!(!state(&app).can_apply()); - send(&mut app, Event::Shape(minimum.into(), Value::Null)); + send(&mut app, RowEvent::Shape(minimum.into(), Value::Null)); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( state(&app).draft.as_ref().unwrap().inputs[maximum].raw, @@ -173,7 +175,7 @@ fn scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers( FacetSelector::Field(FieldFacet::Minimum), SemanticDecision::Candidate, ); - send(&mut app, Event::Input(minimum.clone(), "-".into())); + send(&mut app, RowEvent::Input(minimum.clone(), "-".into())); decide( &mut app, FacetSelector::Field(FieldFacet::Minimum), @@ -184,11 +186,11 @@ fn scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers( FacetSelector::Field(FieldFacet::Default), SemanticDecision::Candidate, ); - send(&mut app, Event::Input(first.clone(), "-".into())); - send(&mut app, Event::Input(second.clone(), "23".into())); + send(&mut app, RowEvent::Input(first.clone(), "-".into())); + send(&mut app, RowEvent::Input(second.clone(), "23".into())); send( &mut app, - Event::Append(default.clone(), json!({"Integer": 3})), + RowEvent::Append(default.clone(), json!({"Integer": 3})), ); assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&first].raw, "-"); assert_eq!( @@ -198,14 +200,14 @@ fn scoped_literal_list_edits_preserve_unfinished_values_and_other_facet_buffers( // Same-variant reselection must not throw away the invalid authored value. send( &mut app, - Event::Variant(format!("{default}/0"), json!({"Integer": 0})), + RowEvent::Variant(format!("{default}/0"), json!({"Integer": 0})), ); assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&first].raw, "-"); - send(&mut app, Event::Array(default.clone(), Some(0), 1)); + send(&mut app, RowEvent::Array(default.clone(), Some(0), 1)); assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&first].raw, "23"); assert_eq!(state(&app).draft.as_ref().unwrap().inputs[&second].raw, "-"); assert!(!state(&app).can_apply()); - send(&mut app, Event::Array(default, Some(1), 0)); + send(&mut app, RowEvent::Array(default, Some(1), 0)); assert!(state(&app).can_apply(), "{:?}", state(&app).error); assert_eq!( state(&app).draft.as_ref().unwrap().inputs[&minimum].raw, @@ -263,8 +265,8 @@ fn independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity assert_ne!(super::view::title(&p, &annotation(vec![])), first_title); } pub(super) fn decide(app: &mut App, selected: FacetSelector, decision: SemanticDecision) { - send(app, Event::Select(selected.clone())); - send(app, Event::Decide(selected, Some(decision))); + send(app, RowEvent::Select(selected.clone())); + send(app, RowEvent::Decide(selected, Some(decision))); } #[test] @@ -304,7 +306,7 @@ fn scalar_keep_take_candidate_commit_exact_source_and_history_in_both_directions ); send( &mut app, - Event::Input(format!("{prefix}/unit"), "kg\n exact suffix ".into()), + RowEvent::Input(format!("{prefix}/unit"), "kg\n exact suffix ".into()), ); let prepared = prepare_semantic_transition(app.project(), state(&app).plan.as_ref().unwrap()) @@ -368,9 +370,9 @@ fn invalid_numeric_draft_and_protected_edits_retain_workflow_and_source() { SemanticDecision::Candidate, ); let path = "/body/Sa/Shared/DataType/definition/Integer/minimum"; - send(&mut app, Event::Input(path.into(), "-".into())); + send(&mut app, RowEvent::Input(path.into(), "-".into())); assert!(!state(&app).can_apply()); - send(&mut app, Event::Tab(Tab::References)); + send(&mut app, RowEvent::Tab(Tab::References)); assert!( state(&app).error.is_some(), "Navigation must retain visible validation errors" @@ -378,8 +380,11 @@ fn invalid_numeric_draft_and_protected_edits_retain_workflow_and_source() { apply(&mut app); assert_eq!(state(&app).draft.as_ref().unwrap().inputs[path].raw, "-"); assert_eq!(app.document.bundle(), &before.bundle); - send(&mut app, Event::Input(path.into(), "2".into())); - send(&mut app, Event::Input("/alias".into(), "unexpected".into())); + send(&mut app, RowEvent::Input(path.into(), "2".into())); + send( + &mut app, + RowEvent::Input("/alias".into(), "unexpected".into()), + ); assert!(!state(&app).can_apply()); assert!( state(&app) @@ -389,7 +394,7 @@ fn invalid_numeric_draft_and_protected_edits_retain_workflow_and_source() { .contains("selected review") ); assert_eq!(app.project(), &before.decoded.project); - send(&mut app, Event::Preview); + send(&mut app, RowEvent::Preview); assert!(state(&app).can_apply()); } @@ -404,7 +409,7 @@ fn explicit_trace_selection_updates_only_that_history() { assert!(!state(&app).can_apply()); send( &mut app, - Event::Trace(Choice { + RowEvent::Trace(Choice { id: second, label: "Independent assessment".into(), }), @@ -469,7 +474,7 @@ fn stale_plan_and_source_writeback_failure_do_not_lose_decisions_or_history() { ); send( &mut app, - Event::Input( + RowEvent::Input( "/body/Sa/Shared/DataType/unit".into(), "long final unit ".repeat(40), ), @@ -517,19 +522,19 @@ fn candidate_mixed_with_reference_take_uses_typed_correspondence_not_text_guessi assert!(!state(&app).can_apply()); send( &mut app, - Event::ReferenceOa(Choice { + RowEvent::ReferenceOa(Choice { id: parent_oa, label: "OA parent".into(), }), ); send( &mut app, - Event::ReferenceSa(Choice { + RowEvent::ReferenceSa(Choice { id: parent_sa, label: "SA parent".into(), }), ); - send(&mut app, Event::AddReference); + send(&mut app, RowEvent::AddReference); decide( &mut app, facet(DataFacet::Unit), @@ -537,7 +542,7 @@ fn candidate_mixed_with_reference_take_uses_typed_correspondence_not_text_guessi ); send( &mut app, - Event::Input( + RowEvent::Input( "/body/Sa/Shared/DataType/unit".into(), parent_oa.to_string(), ), @@ -567,7 +572,7 @@ fn read_only_structured_facts_and_partial_rollup_are_honest() { start(&mut app, oa); send( &mut app, - Event::Decide( + RowEvent::Decide( FacetSelector::Literal(KeyPair { oa: Some("Unpaired".into()), sa: None, @@ -594,7 +599,7 @@ fn read_only_structured_facts_and_partial_rollup_are_honest() { let mut request = State::new(app.project(), trace).unwrap(); request.update( app.project(), - Event::Select(facet(DataFacet::IntegerMaximum)), + RowEvent::Select(facet(DataFacet::IntegerMaximum)), ); let _ = request.view(app.project()); assert_eq!( @@ -630,7 +635,7 @@ fn first_equal_constructor_acknowledges_only_kind_and_allows_independent_leaves( ); send( &mut app, - Event::Input("/body/Sa/Shared/DataType/unit".into(), "local unit".into()), + RowEvent::Input("/body/Sa/Shared/DataType/unit".into(), "local unit".into()), ); assert!( state(&app).can_apply(), @@ -709,7 +714,7 @@ fn state_kind_scalar_uses_typed_candidate_and_core_validation() { ); send( &mut app, - Event::Shape("/body/Sa/Shared/State/kind".into(), json!("Final")), + RowEvent::Shape("/body/Sa/Shared/State/kind".into(), json!("Final")), ); assert!( state(&app).can_apply(), @@ -769,7 +774,7 @@ fn relation_candidates_preserve_endpoints_and_combine_selected_roles_and_bounds( ); send( &mut app, - Event::Shape("/body/DataAssociation/target_maximum".into(), json!(4)), + RowEvent::Shape("/body/DataAssociation/target_maximum".into(), json!(4)), ); assert!(state(&app).can_apply(), "{:?}", state(&app).error); apply(&mut app); @@ -802,7 +807,7 @@ fn deselected_invalid_candidate_is_retained_but_does_not_poison_another_selected facet(DataFacet::IntegerMinimum), SemanticDecision::Candidate, ); - send(&mut app, Event::Input(path.into(), "-".into())); + send(&mut app, RowEvent::Input(path.into(), "-".into())); decide( &mut app, facet(DataFacet::IntegerMinimum), @@ -815,7 +820,7 @@ fn deselected_invalid_candidate_is_retained_but_does_not_poison_another_selected ); send( &mut app, - Event::Input( + RowEvent::Input( "/body/Sa/Shared/DataType/unit".into(), "reviewed local".into(), ), @@ -948,7 +953,7 @@ fn inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approva assert_eq!(impacts.len(), 1); assert_eq!(impacts[0].object, message); let exact = impacts[0].clone(); - send(&mut app, Event::AcceptImpact(0, true)); + send(&mut app, RowEvent::AcceptImpact(0, true)); assert!( state(&app).can_apply(), "{:?}", @@ -973,7 +978,7 @@ fn inherited_annotation_impact_requires_exact_acceptance_and_edits_clear_approva .is_empty() ); assert!(!state(&app).can_apply()); - send(&mut app, Event::AcceptImpact(0, true)); + send(&mut app, RowEvent::AcceptImpact(0, true)); let expected = state(&app).proposed.clone().unwrap(); assert_eq!(app.document.bundle(), &before.bundle); apply(&mut app); diff --git a/crates/oa-desktop/src/semantic_review/view.rs b/crates/oa-desktop/src/semantic_review/view.rs index 73595631..70b8c43e 100644 --- a/crates/oa-desktop/src/semantic_review/view.rs +++ b/crates/oa-desktop/src/semantic_review/view.rs @@ -1,3 +1,4 @@ +use super::row::form_event; use super::*; use crate::{style, views::short}; use iced::{ @@ -7,10 +8,17 @@ use iced::{ }, }; -fn event(event: Event) -> Message { - Message::Workflow(crate::workflow::Event::Semantic(event)) +#[cfg(test)] +#[path = "view_reserved_artifact_tests.rs"] +mod reserved_artifact_tests; + +fn event(event: RowEvent) -> Message { + Message::Workflow(crate::workflow::Event::Semantic(Event::Editor(event))) } -fn action<'a>(label: impl Into, event_value: Event) -> iced::widget::Button<'a, Message> { +fn action<'a>( + label: impl Into, + event_value: RowEvent, +) -> iced::widget::Button<'a, Message> { button(text(label.into()).size(12)) .padding([7, 9]) .style(style::nav) @@ -22,7 +30,8 @@ pub(super) fn choice(project: &Project, id: Id) -> Choice { .get(&id) .map(|v| v.alias.as_str()) .or_else(|| project.relations.get(&id).map(|v| v.alias.as_str())) - .or_else(|| project.artifact_links.get(&id).map(|v| v.alias.as_str())); + .or_else(|| project.artifact_links.get(&id).map(|v| v.alias.as_str())) + .or_else(|| project.traces.get(&id).map(|v| v.alias.as_str())); Choice { id, label: alias.map_or_else( @@ -51,6 +60,7 @@ pub(super) fn group(facet: &FacetSelector) -> &'static str { FacetSelector::Data(_) | FacetSelector::Field(_) | FacetSelector::Literal(_) => { "Data and defaults" } + FacetSelector::Category(_) => "Exchange categories", FacetSelector::Port(_) | FacetSelector::Interface(_) | FacetSelector::ExchangeItem(_) => { "Contracts and interfaces" } @@ -236,45 +246,99 @@ pub(super) fn observed(project: &Project, fact: Option<&FacetReview>, layer: Lay } } -impl State { - pub fn view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { - let trace = self.trace.and_then(|id| project.traces.get(&id)); - let mut traces: Vec<_> = project - .traces - .values() - .filter(|t| t.oa == self.anchor || t.sa == self.anchor) - .map(|t| Choice { - id: t.id, - label: format!( - "{} · {:?} · {} ↔ {} · @{}", - t.name, - t.kind, - project.named(t.oa), - project.named(t.sa), - t.alias - ), - }) - .collect(); - traces.sort_by(|a, b| a.label.cmp(&b.label)); - let selected_trace = traces.iter().find(|c| Some(c.id) == self.trace).cloned(); - let direction = if self.direction == TransitionDirection::OaToSa { - "Use Operational changes in System" +impl ReviewRow { + pub(super) fn review_title(&self, project: &Project, facet: &FacetSelector) -> String { + if let FacetSelector::Artifact { keys, facet } = facet { + let label = |id: Option| { + id.map(|id| self.review_choice(project, id).label) + .unwrap_or_else(|| "Absent".into()) + }; + format!( + "Artifact {} · {} ↔ {}", + words(&format!("{facet:?}")), + label(keys.oa), + label(keys.sa) + ) } else { - "Use System changes in Operational" - }; + title(project, facet) + } + } + + pub(super) fn review_choice(&self, project: &Project, id: Id) -> Choice { + self.artifacts.choice(project, id) + } + + pub(super) fn view<'a>( + &'a self, + project: &'a Project, + header: Element<'a, Message>, + can_apply: bool, + ) -> Element<'a, Message> { + let trace = self.trace.and_then(|id| project.traces.get(&id)); let mut content = column![ - text("Review semantics").size(22), + header, row![ - pick_list(traces, selected_trace, |c| event(Event::Trace(c))).placeholder("Choose a trace / review history").text_size(12).padding(7).width(Fill), - action(direction, Event::Direction(if self.direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa })), - ].spacing(8), - text("Changing trace or direction starts a new review draft. Other pending edits remain separate.").size(10).color(style::MUTED), - row![action("Properties", Event::Tab(Tab::Facts)), action("Members / rows", Event::Tab(Tab::Members)), action(format!("History ({})", self.resets.len()), Event::Tab(Tab::History)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6).wrap(), - ].spacing(8); + action("Properties", RowEvent::Tab(Tab::Facts)), + action("Members / rows", RowEvent::Tab(Tab::Members)), + action("Attachments", RowEvent::Tab(Tab::Attachments)), + action("Artifact links", RowEvent::Tab(Tab::Artifacts)), + action( + format!( + "History ({}) · {} resets", + trace + .and_then(|t| t.baseline.as_ref()) + .and_then(|b| b.semantic.as_ref()) + .map_or(0, |b| b.facets.len()), + self.resets.len() + ), + RowEvent::Tab(Tab::History) + ), + action( + format!("Reference pairs ({})", self.references.len()), + RowEvent::Tab(Tab::References) + ), + action( + format!( + "Additional changes ({})", + self.plan.as_ref().map_or(0, |p| p.impacts.len()) + ), + RowEvent::Tab(Tab::Impacts) + ), + Space::new().width(Fill), + action("Refresh preview", RowEvent::Preview) + ] + .spacing(6) + .wrap(), + ] + .spacing(8); let body = match self.tab { Tab::Facts => self.facts_view(project), Tab::References => self.references_view(project), Tab::Members => self.members_view(project), + Tab::Artifacts => { + if let Some(trace) = trace { + scrollable( + artifacts_view::pairs(&self.artifacts, project, trace) + .map(|e| event(RowEvent::Artifact(e))), + ) + .height(Fill) + .into() + } else { + text("Choose a trace first.").into() + } + } + Tab::Attachments => { + if let Some(trace) = trace { + scrollable( + attachments_view::pairs(&self.attachments, project, trace) + .map(|e| event(RowEvent::Attachment(e))), + ) + .height(Fill) + .into() + } else { + text("Choose a trace first.").into() + } + } Tab::History => self.history_view(project), Tab::Impacts => self.impacts_view(project), }; @@ -300,7 +364,15 @@ impl State { .iter() .filter(|f| f.drift == Drift::Unreviewed) .count(); - content = content.push(text(format!("{} decisions · {} exact resets · {unreviewed} unreviewed facts · Attached-record and multi-source decisions remain pending.", self.decisions.len(), self.resets.len())).size(10).color(style::MUTED)); + content = content.push( + text(format!( + "{} decisions · {} exact resets · {unreviewed} unreviewed facts", + self.decisions.len(), + self.resets.len() + )) + .size(10) + .color(style::MUTED), + ); if let Some(trace) = trace { content = content.push(text(summary(project, trace)).size(10).color(style::MUTED)); } @@ -308,7 +380,7 @@ impl State { .push( row![ text( - "Apply updates the selected history and model together; Undo restores both." + "Apply updates all included histories and the model once; Undo restores the whole batch." ) .size(10) .width(Fill), @@ -319,7 +391,7 @@ impl State { button(text("Apply reviewed changes").size(12)) .padding(8) .on_press_maybe( - self.can_apply() + can_apply .then_some(Message::Workflow(crate::workflow::Event::Apply)) ), ] @@ -334,7 +406,7 @@ impl State { checkbox(self.show_unchanged) .label("Include unchanged facts") .text_size(11) - .on_toggle(|v| event(Event::ShowUnchanged(v))) + .on_toggle(|v| event(RowEvent::ShowUnchanged(v))) ] .spacing(5); let mut groups: BTreeMap<&str, Vec<&FacetDrift>> = BTreeMap::new(); @@ -359,7 +431,7 @@ impl State { list = list.push( button( column![ - text(short(&title(project, &fact.selector), 52)).size(12), + text(short(&self.review_title(project, &fact.selector), 52)).size(12), text(format!("{mark}{}", drift(fact.drift))).size(10) ] .spacing(3), @@ -371,13 +443,17 @@ impl State { } else { style::nav }) - .on_press(event(Event::Select(fact.selector.clone()))), + .on_press(event(RowEvent::Select(fact.selector.clone()))), ); } } row![ - scrollable(list).height(305).width(260), - scrollable(self.detail(project)).height(305).width(Fill) + scrollable(list.padding(iced::Padding::default().right(18))) + .height(305) + .width(260), + scrollable(container(self.detail(project)).padding(iced::Padding::default().right(18))) + .height(305) + .width(Fill) ] .spacing(12) .into() @@ -406,7 +482,7 @@ impl State { .map(|f| { format!( "{}: {}", - title(project, &f.selector()), + self.review_title(project, &f.selector()), observed(project, Some(f), layer) ) }) @@ -455,7 +531,7 @@ impl State { .width(Fill) }; let mut detail = column![ - text(title(project, &fact.selector)).size(17), + text(self.review_title(project, &fact.selector)).size(17), text(drift(fact.drift)).size(11), row![side(Layer::Oa, "Operational"), side(Layer::Sa, "System")].spacing(14) ] @@ -466,12 +542,15 @@ impl State { checkbox(self.resets.contains(&facet)) .label("Reset this exact previous observation before review") .text_size(11) - .on_toggle(move |v| event(Event::Reset(facet.clone(), v))), + .on_toggle(move |v| event(RowEvent::Reset(facet.clone(), v))), ); } - if keyed::supported(&fact.selector) { + if keyed::supported(&fact.selector) + || attachments::supported(&fact.selector) + || artifacts::supported(&fact.selector) + { detail = detail.push(text("Review changes only this collection entry; referenced objects stay intact. Take preserves a row with no source identity. An explicit source identity that is absent can remove its mapped destination entry. Changing a row key requires separate removal and addition.").size(11)) - .push(action("Choose / change exact row identities…", Event::EditPair(fact.selector.clone()))); + .push(action("Choose / change exact row identities…", RowEvent::EditPair(fact.selector.clone()))); if let Some(baseline) = trace .and_then(|t| t.baseline.as_ref()) .and_then(|b| b.semantic.as_ref()) @@ -483,10 +562,10 @@ impl State { checkbox(self.resets.contains(&prior)) .label(format!( "Reset overlapping history: {}", - title(project, &prior) + self.review_title(project, &prior) )) .text_size(11) - .on_toggle(move |v| event(Event::Reset(prior.clone(), v))), + .on_toggle(move |v| event(RowEvent::Reset(prior.clone(), v))), ); } } @@ -553,25 +632,33 @@ impl State { let mut buttons = row![ action( format!("Keep {destination}"), - Event::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget)) + RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget)) ), action( format!("Take {source}"), - Event::Decide(fact.selector.clone(), Some(SemanticDecision::TakeSource)) + RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::TakeSource)) ) ] .spacing(5); - buttons = buttons.push(action( - if is_group { - "Set final kind and payload…" - } else { - "Set final value…" - }, - Event::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)), - )); + if !matches!( + fact.selector, + FacetSelector::Artifact { + facet: ArtifactFacet::Presence, + .. + } + ) { + buttons = buttons.push(action( + if is_group { + "Set final kind and payload…" + } else { + "Set final value…" + }, + RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)), + )); + } detail = detail.push(buttons.wrap()).push(action( "Leave unselected", - Event::Decide(fact.selector.clone(), None), + RowEvent::Decide(fact.selector.clone(), None), )); if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) && keyed::supported(&fact.selector) @@ -583,7 +670,7 @@ impl State { "Include in destination collection (unchecked removes only this entry)", ) .text_size(12) - .on_toggle(move |v| event(Event::Presence(facet.clone(), v))), + .on_toggle(move |v| event(RowEvent::Presence(facet.clone(), v))), ); if keyed_rows::supported(&fact.selector) && let Some(draft) = self.row_drafts.get(&fact.selector) @@ -596,7 +683,7 @@ impl State { iced::widget::text_editor(&draft.editor) .placeholder("Expression") .on_action(move |action| { - event(Event::RowText(facet.clone(), action)) + event(RowEvent::RowText(facet.clone(), action)) }) .size(12) .height(90) @@ -612,7 +699,7 @@ impl State { false, ), draft.local.clone(), - move |value| event(Event::RowLocal(facet.clone(), value)), + move |value| event(RowEvent::RowLocal(facet.clone(), value)), ) .placeholder("Local lifeline in destination scenario") .text_size(12) @@ -625,6 +712,27 @@ impl State { } } if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) + && let Some(editor) = attachments_view::candidate( + &self.attachments, + project, + &fact.selector, + self.direction.target_layer(), + ) + { + detail = detail.push(editor.map(form_event)); + } + if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) + && let Some(editor) = + artifacts_view::candidate(&self.artifacts, project, &fact.selector) + { + detail = detail.push(editor.map(|e| event(RowEvent::Artifact(e)))); + } + if artifacts::supported(&fact.selector) { + detail = detail.push(text("Metadata is reviewed from this initially attached owner. Owner moves and source-absence deletion must agree with kept Presence. Creating a reserved counterpart requires Presence plus all seven metadata decisions; each raw draft remains independent.").size(11)); + } + if !attachments::supported(&fact.selector) + && !artifacts::supported(&fact.selector) + && self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) && let Some(draft) = self.candidate_draft(&fact.selector) && let Some(paths) = self .target(project) @@ -652,15 +760,15 @@ impl State { } } else if self.can_acknowledge_constructor(project, &fact.selector) { detail = detail.push(text("Both constructor kinds are equal and have no prior observation. Acknowledge only the kind; its properties remain independent.").size(12)) - .push(action("Acknowledge equal kind only", Event::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget)))) - .push(action("Leave unselected", Event::Decide(fact.selector.clone(), None))); + .push(action("Acknowledge equal kind only", RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget)))) + .push(action("Leave unselected", RowEvent::Decide(fact.selector.clone(), None))); } else { detail = detail.push(text(if scalar::grouped(&fact.selector) { "This constructor is unavailable on one of the current counterparts. Review the outer constructor before its nested kind." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED)); } if self.staged.contains(&fact.selector) { detail = detail.push(action( "Remove staged address and its decision", - Event::DropStaged(fact.selector.clone()), + RowEvent::DropStaged(fact.selector.clone()), )); } let overlaps: Vec<_> = self @@ -677,22 +785,31 @@ impl State { detail = detail.push(text("Related decisions are selected. Paired rows cannot review the same OA or SA identity twice. Replacing groups require one combined decision; first equal-kind acknowledgments may coexist with descendants. Resolve overlaps explicitly; all drafts are retained.").size(11)); for other in overlaps { detail = detail.push(action( - format!("Leave {} unselected", title(project, other)), - Event::Decide(other.clone(), None), + format!("Leave {} unselected", self.review_title(project, other)), + RowEvent::Decide(other.clone(), None), )); } detail = detail.push(action( "Leave this decision unselected; use the related decisions", - Event::Decide(fact.selector.clone(), None), + RowEvent::Decide(fact.selector.clone(), None), )); } if (is_group || keyed::supported(&fact.selector) + || artifacts::supported(&fact.selector) || matches!(fact.selector, FacetSelector::Order(_)) || constructors::selectors().iter().any(|group| { self.decisions.contains_key(group) && constructors::contains(group, &fact.selector) })) - && let Some(review) = self.plan.as_ref().and_then(|p| p.reviews.first()) + && let Some(review) = self.plan.as_ref().and_then(|plan| { + let trace = project.traces.get(&self.trace?)?; + let source = if self.direction == TransitionDirection::OaToSa { + trace.oa + } else { + trace.sa + }; + plan.reviews.iter().find(|review| review.source == source) + }) { detail = detail.push(text("Exact review scope from preview").size(13)); for (label, selectors) in [ @@ -707,7 +824,7 @@ impl State { } else { selectors .iter() - .map(|s| title(project, s)) + .map(|s| self.review_title(project, s)) .collect::>() .join(", ") } @@ -733,13 +850,13 @@ impl State { let mut content = column![ text("Review an individual member or row").size(16), text("Choose an exact address, then review its value and presence in Properties. This also stages a new row in an empty collection. No model object is created.").size(12), - pick_list(families, Some(self.member_family), |v| event(Event::MemberFamily(v))).text_size(12).padding(7), + pick_list(families, Some(self.member_family), |v| event(RowEvent::MemberFamily(v))).text_size(12).padding(7), ].spacing(9); if let keyed::Family::Member(collection) = self.member_family && let Some(which) = ordered::ordered(collection) { content = content.push(text("Membership and order are separate decisions. An incoming member with ambiguous placement requires an explicit final order; referenced objects are not created or reparented here.").size(11)) - .push(action("Review final order…", Event::Select(FacetSelector::Order(which)))); + .push(action("Review final order…", RowEvent::Select(FacetSelector::Order(which)))); } if !available { content = content.push(text("This collection is not available on the current counterparts. Choose an available collection above; existing staged addresses remain removable below.").size(12)); @@ -749,31 +866,33 @@ impl State { .push(self.annotation_address_view(project, Layer::Sa)); } else if self.member_family == keyed::Family::Literal { content = content.push(text("Literal identity is exact, including case and spaces. Both sides use the same string; a rename requires separate old/new rows.").size(11)) - .push(text_input("Exact literal", &self.literal).on_input(|v| event(Event::Literal(v))).size(12).padding(7)) - .push(row![checkbox(self.literal_oa).label("Operational identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Oa,v))), checkbox(self.literal_sa).label("System identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Sa,v)))].spacing(12)); + .push(text_input("Exact literal", &self.literal).on_input(|v| event(RowEvent::Literal(v))).size(12).padding(7)) + .push(row![checkbox(self.literal_oa).label("Operational identity").text_size(12).on_toggle(|v| event(RowEvent::LiteralSide(Layer::Oa,v))), checkbox(self.literal_sa).label("System identity").text_size(12).on_toggle(|v| event(RowEvent::LiteralSide(Layer::Sa,v)))].spacing(12)); } else { content = content.push(text("Identities may exist outside the collection. Choosing both explicitly sets their OA → SA correspondence. No identity means an unpaired side, distinct from an absent member.").size(11)) - .push(row![pick_list(self.member_choices(project, Layer::Oa), self.member_oa.clone(), |v| event(Event::MemberOa(Some(v)))).placeholder("Operational identity").text_size(12).padding(7).width(Fill), action("No OA identity", Event::MemberOa(None))].spacing(7)) - .push(row![pick_list(self.member_choices(project, Layer::Sa), self.member_sa.clone(), |v| event(Event::MemberSa(Some(v)))).placeholder("System identity").text_size(12).padding(7).width(Fill), action("No SA identity", Event::MemberSa(None))].spacing(7)); + .push(row![pick_list(self.member_choices(project, Layer::Oa), self.member_oa.clone(), |v| event(RowEvent::MemberOa(Some(v)))).placeholder("Operational identity").text_size(12).padding(7).width(Fill), action("No OA identity", RowEvent::MemberOa(None))].spacing(7)) + .push(row![pick_list(self.member_choices(project, Layer::Sa), self.member_sa.clone(), |v| event(RowEvent::MemberSa(Some(v)))).placeholder("System identity").text_size(12).padding(7).width(Fill), action("No SA identity", RowEvent::MemberSa(None))].spacing(7)); } - content = content.push(button(text("Use these addresses and review row").size(12)).padding([7,9]).style(style::nav).on_press_maybe(available.then_some(event(Event::StageMember)))) + content = content.push(button(text("Use these addresses and review row").size(12)).padding([7,9]).style(style::nav).on_press_maybe(available.then_some(event(RowEvent::StageMember)))) .push(text("Changing a row address retains its earlier decisions and drafts. Reset conflicting historical observations and leave overlapping decisions unselected explicitly.").size(11).color(style::MUTED)); if !self.staged.is_empty() { content = content.push(text("Staged addresses in this draft").size(13)); for facet in &self.staged { content = content.push( row![ - text(title(project, facet)).size(11).width(Fill), + text(self.review_title(project, facet)).size(11).width(Fill), action( "Remove address / decision", - Event::DropStaged(facet.clone()) + RowEvent::DropStaged(facet.clone()) ) ] .spacing(7), ); } } - scrollable(content).height(305).into() + scrollable(content.padding(iced::Padding::default().right(18))) + .height(305) + .into() } fn order_view<'a>( @@ -783,23 +902,27 @@ impl State { ) -> Element<'a, Message> { let mut content = column![text("Final destination order").size(13), text("Up/Down changes only positions. Reconcile members explicitly after changing presence: retain surviving positions, remove excluded entries and append newly included entries.").size(11), - action("Reconcile with selected membership", Event::OrderReconcile(which))].spacing(6); + action("Reconcile with selected membership", RowEvent::OrderReconcile(which))].spacing(6); if let Some(ids) = self.order_drafts.get(&which) { if ids.is_empty() { content = content.push(text("Empty order").size(12)); } for (index, id) in ids.iter().enumerate() { let up = button(text("Up").size(11)).padding(5).on_press_maybe( - (index > 0).then_some(event(Event::OrderMove(which, index, -1))), + (index > 0).then_some(event(RowEvent::OrderMove(which, index, -1))), ); let down = button(text("Down").size(11)).padding(5).on_press_maybe( - (index + 1 < ids.len()).then_some(event(Event::OrderMove(which, index, 1))), + (index + 1 < ids.len()).then_some(event(RowEvent::OrderMove(which, index, 1))), ); content = content.push( row![ - text(format!("{}. {}", index + 1, choice(project, *id).label)) - .size(12) - .width(Fill), + text(format!( + "{}. {}", + index + 1, + self.review_choice(project, *id).label + )) + .size(12) + .width(Fill), up, down ] @@ -811,12 +934,12 @@ impl State { let missing: Vec<_> = members .iter() .filter(|id| !ids.contains(id)) - .map(|id| choice(project, *id).label) + .map(|id| self.review_choice(project, *id).label) .collect(); let excluded: Vec<_> = ids .iter() .filter(|id| !members.contains(id)) - .map(|id| choice(project, *id).label) + .map(|id| self.review_choice(project, *id).label) .collect(); if !missing.is_empty() { content = content.push( @@ -865,8 +988,8 @@ impl State { } content = content.push( row![ - action("Root fields", Event::AnnotationBrowse(layer, None, 0)), - action("No identity", Event::AnnotationAddress(layer, None)), + action("Root fields", RowEvent::AnnotationBrowse(layer, None, 0)), + action("No identity", RowEvent::AnnotationAddress(layer, None)), ] .spacing(6), ); @@ -887,7 +1010,7 @@ impl State { ) .push(action( "Up", - Event::AnnotationBrowse(layer, crate::data_values::parent(scope), 0), + RowEvent::AnnotationBrowse(layer, crate::data_values::parent(scope), 0), )); } match self.annotation_choices(project, layer) { @@ -907,7 +1030,7 @@ impl State { .cloned(); content = content.push( pick_list(visible, selected, move |choice| { - event(Event::AnnotationAddress(layer, Some(choice.key))) + event(RowEvent::AnnotationAddress(layer, Some(choice.key))) }) .placeholder("Choose a field") .text_size(12) @@ -917,7 +1040,7 @@ impl State { if let Some(selected) = &address.selected { content = content.push(action( "Browse fields within selected record", - Event::AnnotationBrowse(layer, Some(selected.clone()), 0), + RowEvent::AnnotationBrowse(layer, Some(selected.clone()), 0), )); } let mut navigation = @@ -925,13 +1048,13 @@ impl State { if page > 0 { navigation = navigation.push(action( "Previous", - Event::AnnotationBrowse(layer, address.scope.clone(), page - 1), + RowEvent::AnnotationBrowse(layer, address.scope.clone(), page - 1), )); } if page + 1 < pages { navigation = navigation.push(action( "Next", - Event::AnnotationBrowse(layer, address.scope.clone(), page + 1), + RowEvent::AnnotationBrowse(layer, address.scope.clone(), page + 1), )); } content = content.push(navigation); @@ -951,11 +1074,12 @@ impl State { content = content.push(text("No previous semantic observations on this trace.").size(12)); } + content = content.push(action("Restore reviewed reference pairs", RowEvent::RestoreReviewedReferences)).push(text("Restores exact historical key pairs only. Ordered lists do not identify counterpart pairs. Pending resets are excluded.").size(11)); if let Some(baseline) = baseline { for fact in &baseline.facets { let facet = fact.selector(); let mut item = column![ - text(title(project, &facet)).size(13), + text(self.review_title(project, &facet)).size(13), text(format!( "Operational: {}\nSystem: {}", observed(project, Some(fact), Layer::Oa), @@ -969,7 +1093,7 @@ impl State { checkbox(self.resets.contains(&facet)) .label("Reset this exact observation") .text_size(11) - .on_toggle(move |v| event(Event::Reset(facet.clone(), v))), + .on_toggle(move |v| event(RowEvent::Reset(facet.clone(), v))), ); } else { item = item.push( @@ -987,25 +1111,27 @@ impl State { checkbox(true) .label(format!( "Pending reset no longer in current history: {}", - title(project, &facet) + self.review_title(project, &facet) )) .text_size(11) - .on_toggle(move |v| event(Event::Reset(facet.clone(), v))), + .on_toggle(move |v| event(RowEvent::Reset(facet.clone(), v))), ); } } - scrollable(content).height(305).into() + scrollable(content.padding(iced::Padding::default().right(18))) + .height(305) + .into() } fn references_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { let mut content = column![ text("Reference correspondence · always Operational → System").size(15), text("Choose explicit counterpart identities before taking reference values. Names do not establish correspondence. Changing an already reviewed keyed pair requires an exact reset in History. Existing decisions are retained; resolve their overlapping row identities explicitly.").size(12), - text_input("Filter reference choices by name or alias", &self.reference_search).on_input(|v| event(Event::ReferenceSearch(v))).padding(7).size(12), + text_input("Filter reference choices by name or alias", &self.reference_search).on_input(|v| event(RowEvent::ReferenceSearch(v))).padding(7).size(12), row![ - pick_list(self.all_references(project, Layer::Oa), self.reference_oa.clone(), |c| event(Event::ReferenceOa(c))).placeholder("Operational reference").padding(7).text_size(12).width(Fill), - pick_list(self.all_references(project, Layer::Sa), self.reference_sa.clone(), |c| event(Event::ReferenceSa(c))).placeholder("System reference").padding(7).text_size(12).width(Fill), - action("Use pair", Event::AddReference), + pick_list(self.all_references(project, Layer::Oa), self.reference_oa.as_ref().map(|c| self.review_choice(project, c.id)), |c| event(RowEvent::ReferenceOa(c))).placeholder("Operational reference").padding(7).text_size(12).width(Fill), + pick_list(self.all_references(project, Layer::Sa), self.reference_sa.as_ref().map(|c| self.review_choice(project, c.id)), |c| event(RowEvent::ReferenceSa(c))).placeholder("System reference").padding(7).text_size(12).width(Fill), + action("Use pair", RowEvent::AddReference), ].spacing(7).width(Fill), ].spacing(9).width(Fill); for (oa, sa) in &self.references { @@ -1013,12 +1139,12 @@ impl State { row![ text(format!( "{}\n↔ {}", - choice(project, *oa).label, - choice(project, *sa).label + self.review_choice(project, *oa).label, + self.review_choice(project, *sa).label )) .size(12) .width(Fill), - action("Remove pair", Event::RemoveReference(*oa)) + action("Remove pair", RowEvent::RemoveReference(*oa)) ] .spacing(6) .width(Fill), @@ -1036,12 +1162,12 @@ impl State { impact .objects .oa - .map(|id| choice(project, id).label) + .map(|id| self.review_choice(project, id).label) .unwrap_or_else(|| "Absent".into()), impact .objects .sa - .map(|id| choice(project, id).label) + .map(|id| self.review_choice(project, id).label) .unwrap_or_else(|| "Absent".into()), drift(impact.drift), if impact.unreviewed { @@ -1058,7 +1184,11 @@ impl State { Err(error) => content = content.push(text(error).size(12)), } } - scrollable(content).width(Fill).height(305).into() + // The vertical scrollbar overlays the content's right edge. + scrollable(content.padding(iced::Padding::default().right(18))) + .width(Fill) + .height(305) + .into() } fn impacts_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { @@ -1084,7 +1214,7 @@ impl State { checkbox(self.accepted_impacts.contains(impact)) .label(format!("Accept · {}", project.named(impact.object))) .text_size(12) - .on_toggle(move |accepted| event(Event::AcceptImpact( + .on_toggle(move |accepted| event(RowEvent::AcceptImpact( index, accepted ))), text("Before").size(11).color(style::MUTED), @@ -1111,6 +1241,8 @@ impl State { ); } } - scrollable(content).height(305).into() + scrollable(content.padding(iced::Padding::default().right(18))) + .height(305) + .into() } } diff --git a/crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs b/crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs new file mode 100644 index 00000000..6ed4fc02 --- /dev/null +++ b/crates/oa-desktop/src/semantic_review/view_reserved_artifact_tests.rs @@ -0,0 +1,118 @@ +use super::*; + +fn send(state: &mut State, project: &Project, event: RowEvent) { + let addressed = state.address_event(event); + state.update(project, addressed); +} + +#[test] +fn reserved_artifact_facet_and_reference_labels_survive_recovery_without_calling_it_deleted() { + for reverse in [false, true] { + let mut p = Project::empty(); + let oa = p + .add_element(Kind::Activity, Layer::Oa, "Inspect equipment", None) + .unwrap(); + let sa = p + .add_element(Kind::Function, Layer::Sa, "Monitor equipment", None) + .unwrap(); + let source_layer = if reverse { Layer::Sa } else { Layer::Oa }; + let source = Id::new_v4(); + p.artifact_links.insert( + source, + ArtifactLink { + id: source, + alias: "interlock_requirement".into(), + name: "Interlock requirement".into(), + description: String::new(), + owner: if reverse { sa } else { oa }, + relation: ArtifactRelation::Satisfies, + uri: "requirements/interlock".into(), + revision: None, + fragment: None, + }, + ); + let trace = super::super::tests::add_trace(&mut p, oa, sa, "Requirement correspondence"); + let accepted = p.clone(); + let mut state = State::new(&p, trace).unwrap(); + if reverse { + state.update(&p, Event::Direction(TransitionDirection::SaToOa)); + } + send( + &mut state, + &p, + RowEvent::Artifact(artifacts::Event::Pick( + source_layer, + Some(choice(&p, source)), + )), + ); + send( + &mut state, + &p, + RowEvent::Artifact(artifacts::Event::Reserve), + ); + send(&mut state, &p, RowEvent::Artifact(artifacts::Event::Stage)); + let keys = state.artifacts.pair(); + let destination = if reverse { + keys.oa.unwrap() + } else { + keys.sa.unwrap() + }; + let seed = state.artifacts.seeds[&destination].clone(); + let expected = format!("Interlock requirement · reserved @{}", seed.alias); + let stored = state.recovery_snapshot().unwrap(); + let restored = State::from_recovery(&p, stored.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), stored); + for review in [&state, &restored] { + // Header and list use the same row-aware title; reference pairs and + // selected controls use the same row-aware identity resolver. + for which in [ + ArtifactFacet::Presence, + ArtifactFacet::Name, + ArtifactFacet::Owner, + ] { + let title = review.review_title(&p, &artifacts::selector(&keys, which)); + assert!(title.contains(&expected), "{title}"); + assert!(title.contains("@interlock_requirement"), "{title}"); + assert!(!title.contains("Deleted reference"), "{title}"); + } + assert_eq!(review.review_choice(&p, destination).label, expected); + assert!( + review + .references + .iter() + .any(|(oa, sa)| Some(*oa) == keys.oa && Some(*sa) == keys.sa) + ); + let truly_missing = Id::new_v4(); + assert_eq!( + review.review_choice(&p, truly_missing).label, + format!("Deleted reference · {truly_missing}") + ); + let missing_facet = artifacts::selector( + &KeyPair { + oa: Some(source), + sa: Some(truly_missing), + }, + ArtifactFacet::Presence, + ); + assert!( + review + .review_title(&p, &missing_facet) + .contains("Deleted reference") + ); + } + assert_eq!(p, accepted); + assert!(!p.contains(destination)); + // A retained draft must not label an identity reserved after it has + // become an accepted artifact; the accepted name now takes precedence. + let mut created = p.clone(); + let mut accepted_link = seed; + accepted_link.name = "Accepted interlock evidence".into(); + created.artifact_links.insert(destination, accepted_link); + let label = restored.review_choice(&created, destination).label; + assert!(label.contains("Accepted interlock evidence"), "{label}"); + assert!( + !label.contains("reserved") && !label.contains("Deleted reference"), + "{label}" + ); + } +} diff --git a/crates/oa-desktop/src/source_workspace.rs b/crates/oa-desktop/src/source_workspace.rs index 5d2055b1..266cacc8 100644 --- a/crates/oa-desktop/src/source_workspace.rs +++ b/crates/oa-desktop/src/source_workspace.rs @@ -32,6 +32,22 @@ pub struct RecoveryWorkspace { } impl RecoveryWorkspace { + /// Refresh only accepted clean buffers. Dirty baselines and invalid/unapplied + /// workspaces are authored recovery data, not migration input. + pub(crate) fn upgrade_clean_buffers(&mut self, old: &SourceBundle, new: &SourceBundle) { + if !self.applied || self.entry != old.entry { + return; + } + for (path, (text, baseline)) in &mut self.files { + if text == baseline + && old.files.get(path) == Some(text) + && let Some(upgraded) = new.files.get(path) + { + *text = upgraded.clone(); + *baseline = upgraded.clone(); + } + } + } /// Check the same bounds before replacing a checkpoint and before restoring it. pub(crate) fn validate(&self) -> Result<(), String> { if self.files.len() > LIMITS.max_files diff --git a/crates/oa-desktop/src/view_copy.rs b/crates/oa-desktop/src/view_copy.rs new file mode 100644 index 00000000..569b91c4 --- /dev/null +++ b/crates/oa-desktop/src/view_copy.rs @@ -0,0 +1,237 @@ +//! Duplicate presentation identities while retaining the same engineering model. +use oa_core::{Command, Id, Project}; +use std::collections::BTreeSet; + +fn fresh_id(used: &mut BTreeSet) -> Id { + loop { + let id = Id::new_v4(); + if used.insert(id) { + return id; + } + } +} + +fn fresh_alias(used: &mut BTreeSet, original: &str) -> String { + let stem = format!("{original}_copy"); + let mut candidate = stem.clone(); + let mut suffix = 2usize; + while !used.insert(candidate.clone()) { + candidate = format!("{stem}_{suffix}"); + suffix += 1; + } + candidate +} + +/// The returned existing core command creates the whole presentation atomically. +/// It does not copy artifact claims or reidentify any existing model record. +pub fn command(project: &Project, source: Id) -> Result { + let mut diagram = project + .diagrams + .get(&source) + .cloned() + .ok_or("The saved view to duplicate no longer exists.")?; + let identities = project.identities(); + let mut used_ids: BTreeSet<_> = identities.iter().map(|(id, _)| *id).collect(); + let mut used_aliases: BTreeSet<_> = identities + .iter() + .map(|(_, alias)| (*alias).to_owned()) + .collect(); + diagram.id = fresh_id(&mut used_ids); + diagram.alias = fresh_alias(&mut used_aliases, &diagram.alias); + diagram.name = format!("{} copy", diagram.name); + diagram.nodes = diagram + .nodes + .into_values() + .map(|mut node| { + node.id = fresh_id(&mut used_ids); + node.alias = fresh_alias(&mut used_aliases, &node.alias); + (node.id, node) + }) + .collect(); + Ok(Command::CreateDiagram(diagram)) +} + +#[cfg(test)] +mod tests { + use super::*; + use oa_core::{view_options::*, *}; + + fn assert_only_presentation_ids_changed(source: &Diagram, copy: &Diagram) { + assert_ne!(source.id, copy.id); + assert_ne!(source.alias, copy.alias); + assert_eq!(copy.name, format!("{} copy", source.name)); + assert_eq!(copy.description, source.description); + assert_eq!(copy.layer, source.layer); + assert_eq!(copy.projection, source.projection); + assert_eq!(copy.view_options, source.view_options); + assert_eq!(copy.routes, source.routes); + assert_eq!(copy.nodes.len(), source.nodes.len()); + for node in source.nodes.values() { + let copied = copy + .nodes + .values() + .find(|copy| copy.element == node.element) + .unwrap(); + assert_ne!(node.id, copied.id); + assert_ne!(node.alias, copied.alias); + let mut normalized = copied.clone(); + normalized.id = node.id; + normalized.alias.clone_from(&node.alias); + assert_eq!(&normalized, node); + } + } + + #[test] + fn duplicate_retains_ports_routes_options_and_artifact_claims_with_exact_undo_redo() { + let mut project = Project::empty(); + let a = project + .add_element(Kind::Function, Layer::Sa, "Request", None) + .unwrap(); + let b = project + .add_element(Kind::Function, Layer::Sa, "Provide", None) + .unwrap(); + let exchange = project.relate(RelationKind::FunctionalExchange, "Data", a, b); + let port = match project.relations[&exchange].body { + RelationBody::FunctionalExchange { source_port, .. } => source_port, + _ => unreachable!(), + }; + let diagram = project.ensure_diagram(Layer::Sa); + let first = project.place(diagram, a, Position { x: 80., y: 120. }); + project.place(diagram, b, Position { x: 700., y: 240. }); + let view = project.diagrams.get_mut(&diagram).unwrap(); + view.description = "Engineering concern with manual routing".into(); + view.projection = ProjectionKind::FunctionalDataflow; + view.nodes + .get_mut(&first) + .unwrap() + .port_anchors + .push(PortAnchor { + port, + side: PortAnchorSide::Bottom, + offset: 0.35, + }); + view.routes.push(EdgeRoute { + edge: exchange, + waypoints: vec![Position { x: 420., y: 440. }], + label: Some(EdgeLabel { + fraction: 0.4, + offset: Position { x: 8., y: -15. }, + }), + }); + view.view_options = ViewOptions { + hidden_edges: vec![exchange], + work_categories: CategorySelection::Any(vec![]), + work_contexts: ContextSelection::Any(vec![]), + exchange_labels: ExchangeLabels::NameAndItems, + ..ViewOptions::default() + }; + let artifact = Id::new_v4(); + project.artifact_links.insert( + artifact, + ArtifactLink { + id: artifact, + alias: "view_review".into(), + name: "View review".into(), + description: String::new(), + owner: first, + relation: ArtifactRelation::References, + uri: "./artifacts/review.md".into(), + revision: None, + fragment: None, + }, + ); + let original_source = language::format(&project); + let mut session = Session::new(project.clone()); + let copy_id = session + .execute(command(&project, diagram).unwrap()) + .unwrap() + .unwrap(); + let copied = session.project().clone(); + assert_only_presentation_ids_changed( + &project.diagrams[&diagram], + &copied.diagrams[©_id], + ); + let mut normalized = copied.clone(); + normalized.diagrams.remove(©_id); + assert_eq!(normalized, project); + assert_eq!(language::format(&normalized), original_source); + assert_eq!(language::parse(&language::format(&copied)).unwrap(), copied); + session.undo(); + assert_eq!(session.project(), &project); + session.redo(); + assert_eq!(session.project(), &copied); + assert_eq!(copied.artifact_links, project.artifact_links); + } + + #[test] + fn duplicate_state_view_preserves_machine_nesting_and_collapse_with_unique_aliases() { + let mut project = language::parse( + r#"arcadia 3; project operating_modes "Operating modes" { + oa_entity crew "Crew" {} + oa_state_machine modes "Modes" { parent @crew; } + oa_region main "Main" { parent @modes; } + oa_state ready "Ready" { parent @main; } + oa_region nested "Nested" { parent @ready; } + oa_state active "Active" { parent @nested; } + diagram states "Operating modes" { projection { StateMachine { machine @modes; }; }; } + node ready_view { diagram @states; element @ready; } + node main_view { diagram @states; element @main; } + node nested_view { diagram @states; element @nested; } + node active_view { diagram @states; element @active; } + }"#, + ) + .unwrap(); + let diagram = project.diagrams.values().next().unwrap().id; + let ready = project + .elements + .values() + .find(|element| element.alias == "ready") + .unwrap() + .id; + project + .diagrams + .get_mut(&diagram) + .unwrap() + .nodes + .values_mut() + .find(|node| node.element == ready) + .unwrap() + .collapsed = true; + let source = project.diagrams[&diagram].clone(); + let mut session = Session::new(project); + for _ in 0..3 { + let id = session + .execute(command(session.project(), diagram).unwrap()) + .unwrap() + .unwrap(); + assert_only_presentation_ids_changed(&source, &session.project().diagrams[&id]); + let projected = projection::project_view(session.project(), id); + let expected = projection::project_view(session.project(), diagram); + assert_eq!( + projected + .nodes + .iter() + .map(|node| node.element) + .collect::>(), + expected.nodes.iter().map(|node| node.element).collect() + ); + } + let aliases: Vec<_> = session + .project() + .identities() + .into_iter() + .map(|(_, alias)| alias) + .collect(); + assert_eq!(aliases.len(), aliases.iter().collect::>().len()); + assert_eq!( + language::parse(&language::format(session.project())).unwrap(), + *session.project() + ); + } + + #[test] + fn missing_view_does_not_propose_an_empty_replacement() { + let project = Project::empty(); + assert!(command(&project, Id::new_v4()).is_err()); + } +} diff --git a/crates/oa-desktop/src/views.rs b/crates/oa-desktop/src/views.rs index 61a6950d..e15d6179 100644 --- a/crates/oa-desktop/src/views.rs +++ b/crates/oa-desktop/src/views.rs @@ -234,9 +234,15 @@ pub fn view(app: &App) -> View<'_, Message> { } let footer = container( row![ - text(short(if app.busy { "Working…" } else { &app.status }, 145)) - .size(11) - .width(Fill), + container(tooltip( + text(short(if app.busy { "Working…" } else { &app.status }, 145)).size(11), + container(text(if app.busy { "Working…" } else { &app.status }).size(12)) + .padding(10) + .max_width(600) + .style(style::card), + tooltip::Position::Top, + )) + .width(Fill), label(format!( "{} objects · {} links", p.elements.len(), @@ -410,6 +416,7 @@ fn diagram(app: &App) -> View<'_, Message> { let canvas: View<'_, Message> = if let Some(id) = app.active_diagram { let program = DiagramCanvas { project: app.project(), + view_index: Some(app.document.view_index()), diagram: id, selected: app.selected, zoom: app.zoom, @@ -635,6 +642,36 @@ fn inspector(app: &App) -> View<'_, Message> { } } // Keep a discoverable primary action; secondary actions live in Actions. + if p.elements + .get(&app.selected) + .is_some_and(|e| e.layer() == Layer::Oa && matches!(e.kind(), Kind::Entity | Kind::Actor)) + { + let draw = app.page == Page::Diagram + && app.active_diagram.is_some_and(|diagram| { + crate::connection_edit::visible(&projection::project_view(p, diagram)) + .any(|id| id == app.selected) + }); + body = body.push( + button( + text(if draw { + "Draw communication means" + } else { + "Configure communication means…" + }) + .size(12), + ) + .padding(9) + .width(Fill) + .on_press(if draw { + Message::Connection(crate::connection_edit::Event::Begin( + crate::connection_tool::Tool::CommunicationMean, + app.selected, + )) + } else { + Message::AddRelationKind(RelationKind::CommunicationMean, false) + }), + ); + } if p.elements .get(&app.selected) .is_some_and(|e| matches!(e.kind(), Kind::System | Kind::SystemActor)) diff --git a/crates/oa-desktop/src/workflow.rs b/crates/oa-desktop/src/workflow.rs index 89ea2deb..8fb6b8f4 100644 --- a/crates/oa-desktop/src/workflow.rs +++ b/crates/oa-desktop/src/workflow.rs @@ -12,6 +12,7 @@ use iced::{ }; use oa_core::{transitions::*, *}; use std::collections::BTreeSet; +mod communication; pub(crate) mod recovery; #[derive(Debug, Clone)] @@ -31,6 +32,7 @@ pub enum Event { Scope(String), MappingTarget(Id, Choice), MappingKind(Id, Kind), + CommunicationPort(Id, bool, communication::PortMenuChoice), Resolve(Id, TransitionField, FieldDecision), Preview, } @@ -125,10 +127,12 @@ impl Relationship { return validation::can_create_interface_role(p, id); } if self.kind == RelationKind::ExchangeAllocation { - return p - .relations - .get(&id) - .is_some_and(|r| r.kind() == RelationKind::BoundaryExchange); + return p.relations.get(&id).is_some_and(|r| { + matches!( + r.kind(), + RelationKind::BoundaryExchange | RelationKind::CommunicationMean + ) + }); } p.elements.get(&id).is_some_and(|e| { Kind::ALL @@ -187,6 +191,10 @@ impl Relationship { format!("{} {verb} {}", p.named(source), p.named(target)) } fn default_orientation(&mut self, p: &Project) { + if self.kind == RelationKind::CommunicationMean { + self.oriented = false; + return; + } self.oriented = !self .source .as_ref() @@ -230,8 +238,15 @@ impl Relationship { if let RelationBody::CapabilityExtend { condition, .. } = &mut body { *condition = self.condition.text(); } - if let RelationBody::BoundaryExchange { oriented, .. } = &mut body { + if let RelationBody::BoundaryExchange { + oriented, items, .. + } + | RelationBody::CommunicationMean { + oriented, items, .. + } = &mut body + { *oriented = self.oriented; + *items = self.items.clone(); } let commands = vec![Command::CreateRelation(Relation { id, @@ -248,6 +263,7 @@ impl Relationship { pub struct Transition { pub request: TransitionRequest, pub plan: TransitionPlan, + communication_ports: std::collections::BTreeMap, } impl Workflow { pub fn manual_trace(p: &Project, selected: Id) -> Result { @@ -334,7 +350,11 @@ impl Workflow { let mut request = TransitionRequest::new(direction, vec![selected]); request.scope = DependencyScope::Required; let plan = plan_transition(p, &request).map_err(|e| e.to_string())?; - Ok(Self::Transition(Box::new(Transition { request, plan }))) + Ok(Self::Transition(Box::new(Transition { + request, + plan, + communication_ports: Default::default(), + }))) } /// Return a complete accepted candidate only on explicit Apply. Intermediate /// choices and previews never mutate the Session. @@ -439,6 +459,9 @@ impl Workflow { }, Self::Transition(d) => { match event { + Event::CommunicationPort(exchange, source, value) => { + d.choose_communication_port(p, exchange, source, value)?; + } Event::Scope(scope) => { d.request.scope = match scope.as_str() { "Required references" => DependencyScope::Required, @@ -546,6 +569,16 @@ impl Workflow { Event::Apply => { let mut candidate = apply_transition(p, &d.plan).map_err(|e| e.to_string())?; + // Behavior is already usable in its dedicated workspace. An + // unrequested architecture placement would alter an unrelated + // view, including its routes, just to display this operation. + if d.request.destination_diagram.is_none() + && d.request.sources.first().is_some_and(|source| { + crate::behavior::selected_behavior(p, *source).is_some() + }) + { + return Ok(Some(candidate)); + } let layer = d.request.direction.target_layer(); let diagram = d .request @@ -720,10 +753,13 @@ impl Workflow { .padding(9), ); } - if d.kind == RelationKind::BoundaryExchange { + if matches!( + d.kind, + RelationKind::BoundaryExchange | RelationKind::CommunicationMean + ) { content = content.push( checkbox(d.oriented) - .label("Oriented component exchange") + .label("Directed source → target (unchecked: bidirectional)") .on_toggle(|v| Message::Workflow(Event::Oriented(v))), ); } @@ -753,6 +789,14 @@ impl Workflow { .width(Fill), ); } + } + if matches!( + d.kind, + RelationKind::FunctionalExchange + | RelationKind::BoundaryExchange + | RelationKind::CommunicationMean + ) { + content = content.push(text("Declared carried items").size(14)); for item in p .elements .values() @@ -905,6 +949,9 @@ impl Workflow { ); } } + if let Some(ports) = d.communication_view(p, source) { + item = item.push(ports); + } content = content.push( iced::widget::container(item) .padding(12) diff --git a/crates/oa-desktop/src/workflow/communication.rs b/crates/oa-desktop/src/workflow/communication.rs new file mode 100644 index 00000000..91edc391 --- /dev/null +++ b/crates/oa-desktop/src/workflow/communication.rs @@ -0,0 +1,304 @@ +//! Explicit SA endpoint decisions for an operational communication counterpart. +use super::*; +use iced::widget::column; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub(super) struct PortDraft { + source: Option, + target: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PortOption { + Reuse(Id), + Create(ComponentPortKind, ComponentPortOrientation), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PortMenuChoice { + option: PortOption, + label: String, +} + +impl std::fmt::Display for PortMenuChoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.label) + } +} + +fn selected(p: &Project, value: &CommunicationPortChoice) -> PortMenuChoice { + match value { + CommunicationPortChoice::Reuse { port } => PortMenuChoice { + option: PortOption::Reuse(*port), + label: format!("Reuse {}", choice(p, *port).label), + }, + CommunicationPortChoice::Create { + kind, orientation, .. + } => creation(*kind, *orientation), + } +} + +fn creation(kind: ComponentPortKind, orientation: ComponentPortOrientation) -> PortMenuChoice { + PortMenuChoice { + option: PortOption::Create(kind, orientation), + label: match kind { + ComponentPortKind::Standard => "Create Standard port · bidirectional".into(), + ComponentPortKind::Flow => format!("Create Flow port · {orientation:?}"), + }, + } +} + +impl Transition { + pub(super) fn sync_communication_ports(&mut self) { + self.request.communication_endpoints = self + .communication_ports + .iter() + .filter_map(|(id, draft)| { + Some(CommunicationEndpointDecision { + source: *id, + source_port: draft.source.clone()?, + target_port: draft.target.clone()?, + }) + }) + .collect(); + } + pub(super) fn choose_communication_port( + &mut self, + p: &Project, + exchange: Id, + source: bool, + choice: PortMenuChoice, + ) -> Result<(), String> { + if self.request.direction != TransitionDirection::OaToSa + || !p + .relations + .get(&exchange) + .is_some_and(|r| r.kind() == RelationKind::CommunicationMean) + { + return Err("Choose an operational communication means in this transition.".into()); + } + let value = match choice.option { + PortOption::Reuse(port) => CommunicationPortChoice::Reuse { port }, + PortOption::Create(kind, orientation) => { + let id = Id::new_v4(); + let side = if source { "source" } else { "target" }; + let name = format!("{} · {side} port", p.named(exchange)); + CommunicationPortChoice::Create { + id, + alias: p.fresh_alias(&name, "port", id), + name, + description: String::new(), + kind, + orientation, + } + } + }; + let draft = self.communication_ports.entry(exchange).or_default(); + if source { + draft.source = Some(value); + } else { + draft.target = Some(value); + } + self.sync_communication_ports(); + Ok(()) + } + + pub(super) fn communication_view<'a>( + &'a self, + p: &'a Project, + exchange: Id, + ) -> Option> { + if self.request.direction != TransitionDirection::OaToSa { + return None; + } + let relation = p.relations.get(&exchange)?; + if relation.kind() != RelationKind::CommunicationMean { + return None; + } + let mut content = column![ + text("System communication endpoints").size(14), + text("Choose both component ports explicitly. Participant correspondence still maps entities to participants. New ports are created together with the reviewed exchange; interface and functional-port allocation decisions remain explicit.").size(12), + ].spacing(8).width(Fill); + for source in [true, false] { + let participant = if source { + relation.source() + } else { + relation.target() + }; + let owner = self + .request + .mappings + .iter() + .find(|m| m.source == participant) + .and_then(|m| m.target) + .or_else(|| { + self.plan + .changes + .iter() + .find(|c| c.source == participant) + .map(|c| c.target) + }); + let mut options = vec![creation( + ComponentPortKind::Standard, + ComponentPortOrientation::Unset, + )]; + for orientation in [ + ComponentPortOrientation::Out, + ComponentPortOrientation::In, + ComponentPortOrientation::InOut, + ] { + options.push(creation(ComponentPortKind::Flow, orientation)); + } + options.extend( + p.elements + .values() + .filter(|e| { + e.layer() == Layer::Sa + && owner == e.parent + && owner.is_some() + && matches!(port_contract(e), Some(PortContract::Component { .. })) + }) + .map(|e| selected(p, &CommunicationPortChoice::Reuse { port: e.id })), + ); + let draft = self.communication_ports.get(&exchange); + let value = draft + .and_then(|d| { + if source { + d.source.as_ref() + } else { + d.target.as_ref() + } + }) + .or_else(|| { + self.request + .communication_endpoints + .iter() + .find(|d| d.source == exchange) + .map(|d| { + if source { + &d.source_port + } else { + &d.target_port + } + }) + }); + let value = value.map(|v| selected(p, v)); + if let Some(value) = &value + && !options.contains(value) + { + options.push(value.clone()); + } + content = content + .push( + text(format!( + "{} · {}", + if source { + "Source participant" + } else { + "Target participant" + }, + p.named(participant) + )) + .size(12), + ) + .push( + pick_list(options, value, move |choice| { + Message::Workflow(Event::CommunicationPort(exchange, source, choice)) + }) + .placeholder("Choose a component port") + .padding(8) + .text_size(12) + .width(Fill), + ); + } + Some(content.into()) + } +} + +pub(super) fn from_request(request: &TransitionRequest) -> BTreeMap { + request + .communication_endpoints + .iter() + .map(|d| { + ( + d.source, + PortDraft { + source: Some(d.source_port.clone()), + target: Some(d.target_port.clone()), + }, + ) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn communication_requires_two_explicit_ports_and_recovers_the_partial_choice() { + let mut p = Project::empty(); + let oa_a = p + .add_element(Kind::Entity, Layer::Oa, "Controller", None) + .unwrap(); + let oa_b = p + .add_element(Kind::Actor, Layer::Oa, "Partner", None) + .unwrap(); + let sa_a = p + .add_element(Kind::System, Layer::Sa, "Controller", None) + .unwrap(); + let sa_b = p + .add_element(Kind::SystemActor, Layer::Sa, "Partner", None) + .unwrap(); + let exchange = p.relate(RelationKind::CommunicationMean, "Radio", oa_a, oa_b); + let mut workflow = Workflow::transition(&p, exchange).unwrap(); + let Workflow::Transition(t) = &mut workflow else { + unreachable!() + }; + for (source, target) in [(oa_a, sa_a), (oa_b, sa_b)] { + t.request.mappings.push(TransitionMapping { + source, + target: Some(target), + target_kind: None, + }); + } + workflow.update(&p, Event::Preview).unwrap(); + let make_port = || creation(ComponentPortKind::Standard, ComponentPortOrientation::Unset); + workflow + .update(&p, Event::CommunicationPort(exchange, true, make_port())) + .unwrap(); + let Workflow::Transition(t) = &workflow else { + unreachable!() + }; + assert!(t.request.communication_endpoints.is_empty()); + assert!(!t.plan.conflicts.is_empty()); + let raw = workflow.recovery_snapshot().unwrap(); + let mut restored = Workflow::from_recovery(&p, raw.clone()).unwrap(); + assert_eq!(restored.recovery_snapshot().unwrap(), raw); + restored + .update(&p, Event::CommunicationPort(exchange, false, make_port())) + .unwrap(); + let Workflow::Transition(t) = &restored else { + unreachable!() + }; + assert!(t.plan.conflicts.is_empty(), "{:?}", t.plan.conflicts); + let decision = t.request.communication_endpoints[0].clone(); + let CommunicationPortChoice::Create { id: from, .. } = decision.source_port else { + unreachable!() + }; + let CommunicationPortChoice::Create { id: to, .. } = decision.target_port else { + unreachable!() + }; + let before = p.clone(); + let accepted = restored.update(&p, Event::Apply).unwrap().unwrap(); + assert_eq!(accepted.elements[&from].parent, Some(sa_a)); + assert_eq!(accepted.elements[&to].parent, Some(sa_b)); + assert!(accepted.relations.values().any(|r| matches!(r.body, + RelationBody::BoundaryExchange { source, target, oriented: false, .. } + if source == from && target == to))); + assert_eq!(p, before); + } +} diff --git a/crates/oa-desktop/src/workflow/recovery.rs b/crates/oa-desktop/src/workflow/recovery.rs index 5c69cf8c..6567d86c 100644 --- a/crates/oa-desktop/src/workflow/recovery.rs +++ b/crates/oa-desktop/src/workflow/recovery.rs @@ -30,6 +30,8 @@ enum Saved { Transition { base: String, request: TransitionRequest, + #[serde(default)] + communication_ports: std::collections::BTreeMap, }, } pub(crate) fn endpoint(p: &Project, port: PortChoice) -> EndpointChoice { @@ -52,6 +54,40 @@ fn check(p: &Project, base: &str) -> Result<(), String> { Ok(()) } impl Workflow { + pub(crate) fn from_recovery_after_source5_upgrade( + p: &Project, + legacy_base: &str, + value: serde_json::Value, + ) -> Result { + let mut saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?; + match &mut saved { + Saved::Semantic(value) => { + return crate::semantic_review::State::from_recovery_after_source5_upgrade( + p, + legacy_base, + value.take(), + ) + .map(|state| Self::Semantic(Box::new(state))); + } + Saved::Ports(value) => { + return crate::port_workflow::State::from_recovery_after_source5_upgrade( + p, + legacy_base, + value.take(), + ) + .map(|state| Self::Ports(Box::new(state))); + } + Saved::Relation { base, .. } + | Saved::ManualTrace { base, .. } + | Saved::Transition { base, .. } => { + if base != legacy_base { + return Err("Workflow recovery does not match the accepted source-5 model identity and revision.".into()); + } + *base = project_fingerprint(p); + } + } + Self::from_recovery(p, serde_json::to_value(saved).map_err(|e| e.to_string())?) + } pub fn recovery_snapshot(&self) -> Result { let saved = match self { Self::Semantic(state) => Saved::Semantic(state.recovery_snapshot()?), @@ -84,6 +120,7 @@ impl Workflow { Self::Transition(d) => Saved::Transition { base: d.plan.base_fingerprint.clone(), request: d.request.clone(), + communication_ports: d.communication_ports.clone(), }, }; serde_json::to_value(saved).map_err(|e| e.to_string()) @@ -148,10 +185,28 @@ impl Workflow { } Ok(workflow) } - Saved::Transition { base, request } => { + Saved::Transition { + base, + request, + mut communication_ports, + } => { check(p, &base)?; + if communication_ports.len() > crate::source_workspace::LIMITS.model.max_records { + return Err("Recovered transition exceeds endpoint draft limits.".into()); + } + for (id, draft) in communication::from_request(&request) { + communication_ports.entry(id).or_insert(draft); + } let plan = plan_transition(p, &request).map_err(|e| e.to_string())?; - Ok(Self::Transition(Box::new(Transition { request, plan }))) + let mut transition = Transition { + request, + plan, + communication_ports, + }; + transition.sync_communication_ports(); + transition.plan = + plan_transition(p, &transition.request).map_err(|e| e.to_string())?; + Ok(Self::Transition(Box::new(transition))) } } } diff --git a/crates/oa-server/src/routes.rs b/crates/oa-server/src/routes.rs index 69599141..e3db50c7 100644 --- a/crates/oa-server/src/routes.rs +++ b/crates/oa-server/src/routes.rs @@ -1065,6 +1065,19 @@ fn check_commands(commands: &[Command]) -> ApiResult<()> { Ok(()) } +fn check_transition_request(request: &oa_core::transitions::TransitionRequest) -> ApiResult<()> { + if request.sources.len() > 100 + || request.mappings.len() > 200 + || request.communication_endpoints.len() > 200 + || request.resolutions.len() > 400 + { + return Err(ApiError::bad_request( + "Transition requests allow at most 100 sources, 200 mappings or communication endpoint decisions and 400 field resolutions.", + )); + } + Ok(()) +} + async fn preview_transition( State(state): State, id: ProjectPath, @@ -1077,21 +1090,38 @@ async fn preview_transition( let body = bytes(body)?; blocking(state, move |store| { let request: oa_core::transitions::TransitionRequest = json_body(&body)?; - if request.sources.len() > 100 || request.mappings.len() > 200 || request.resolutions.len() > 400 { - return Err(ApiError::bad_request("Transition requests allow at most 100 sources, 200 mappings and 400 field resolutions.")); - } + check_transition_request(&request)?; let snapshot = store.checked(id, &expected)?; let current = &snapshot; - let plan = oa_core::transitions::plan_transition(¤t.model, &request) - .map_err(|error| ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, "transition_rejected", error.to_string()))?; - if !plan.commands.is_empty() { check_commands(&plan.commands)?; } + let plan = + oa_core::transitions::plan_transition(¤t.model, &request).map_err(|error| { + ApiError::new( + StatusCode::UNPROCESSABLE_ENTITY, + "transition_rejected", + error.to_string(), + ) + })?; + if !plan.commands.is_empty() { + check_commands(&plan.commands)?; + } if plan.conflicts.is_empty() { let candidate = oa_core::transitions::prepare_transition(¤t.model, &plan) - .map_err(|error| ApiError::new(StatusCode::UNPROCESSABLE_ENTITY, "transition_rejected", error.to_string()))?; + .map_err(|error| { + ApiError::new( + StatusCode::UNPROCESSABLE_ENTITY, + "transition_rejected", + error.to_string(), + ) + })?; PreparedProject::from_command(current, candidate)?; } - Ok(response(current, StatusCode::OK, json!({"base_revision":current.revision,"plan":plan}))) - }).await + Ok(response( + current, + StatusCode::OK, + json!({"base_revision":current.revision,"plan":plan}), + )) + }) + .await } #[derive(Deserialize)] @@ -1164,6 +1194,7 @@ fn check_semantic_request(request: &oa_core::semantic::SemanticTransitionRequest }); if transition.sources.len() > 100 || transition.mappings.len() > 200 + || transition.communication_endpoints.len() > 200 || transition.resolutions.len() > 400 || request.traces.len() > 100 || request.references.len() > 5_000 @@ -1173,7 +1204,7 @@ fn check_semantic_request(request: &oa_core::semantic::SemanticTransitionRequest || request.accepted_impacts.len() > 200 { return Err(ApiError::bad_request( - "Semantic transitions allow at most 100 sources/traces/reviews, 200 mappings/candidates/accepted impacts, 400 prose resolutions and 5,000 reference mappings or combined facet resolutions/resets.", + "Semantic transitions allow at most 100 sources/traces/reviews, 200 mappings/communication endpoint decisions/candidates/accepted impacts, 400 prose resolutions and 5,000 reference mappings or combined facet resolutions/resets.", )); } Ok(()) @@ -1271,6 +1302,7 @@ async fn commit_transition( let body = bytes(body)?; blocking(state, move |store| { let request: TransitionApplyRequest = json_body(&body)?; + check_transition_request(&request.plan.request)?; let snapshot = store.checked(id, &expected)?; let current = &snapshot; if current.revision != request.base_revision { diff --git a/crates/oa-server/tests/http.rs b/crates/oa-server/tests/http.rs index 0ce74eab..3b8477a7 100644 --- a/crates/oa-server/tests/http.rs +++ b/crates/oa-server/tests/http.rs @@ -8,12 +8,16 @@ use serde_json::{Value, json}; use std::{path::Path, sync::Arc}; use tokio::{sync::oneshot, task::JoinHandle}; +#[path = "http/communication_limits.rs"] +mod communication_limits; #[path = "http/data_contracts.rs"] mod data_contracts; #[path = "http/port_anchors.rs"] mod port_anchors; #[path = "http/semantic_transitions.rs"] mod semantic_transitions; +#[path = "http/source6_views.rs"] +mod source6_views; #[path = "http/source_projects.rs"] mod source_projects; @@ -2015,7 +2019,7 @@ async fn current_migration_reports_and_invalid_typed_transaction_preserve_http_s .text() .await .unwrap(); - assert!(source.starts_with("arcadia 5;")); + assert!(source.starts_with("arcadia 6;")); assert_eq!(oa_core::language::parse(&source).unwrap(), stored.model); let no_op = server .client diff --git a/crates/oa-server/tests/http/communication_limits.rs b/crates/oa-server/tests/http/communication_limits.rs new file mode 100644 index 00000000..c06f15bb --- /dev/null +++ b/crates/oa-server/tests/http/communication_limits.rs @@ -0,0 +1,119 @@ +//! HTTP admission limits must reject endpoint decisions before transition planning. +use super::*; +use oa_core::{semantic::SemanticTransitionRequest, transitions::*}; + +async fn post( + server: &TestServer, + id: Id, + route: &str, + etag: &str, + body: &Value, +) -> reqwest::Response { + server + .client + .post(server.url(&format!("/projects/{id}/{route}"))) + .header("if-match", etag) + .json(body) + .send() + .await + .unwrap() +} + +async fn unchanged(server: &TestServer, before: &StoredProject, disk: &Path, bytes: &[u8]) { + let after = server.get(before.resource_id).await; + assert_eq!(after.revision, before.revision); + assert_eq!(after.etag(), before.etag()); + assert_eq!(after.model, before.model); + assert_eq!(after.source_project, before.source_project); + assert_eq!(std::fs::read(disk).unwrap(), bytes); +} + +async fn endpoint_limit(semantic: bool) { + let directory = tempfile::tempdir().unwrap(); + let server = TestServer::start(directory.path(), None).await; + let mut model = Project::empty(); + let source = model + .add_element(Kind::Activity, Layer::Oa, "Inspect service", None) + .unwrap(); + let (id, etag) = server.import(&model).await; + let before = server.get(id).await; + let disk = directory.path().join(format!("{id}.json")); + let bytes = std::fs::read(&disk).unwrap(); + let transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]); + let (route, mut request, decision_path, applied_decision_path) = if semantic { + ( + "semantic-transitions", + serde_json::to_value(SemanticTransitionRequest::new(transition)).unwrap(), + "/transition/communication_endpoints", + "/plan/request/transition/communication_endpoints", + ) + } else { + ( + "transitions", + serde_json::to_value(transition).unwrap(), + "/communication_endpoints", + "/plan/request/communication_endpoints", + ) + }; + // Obtain a real prepared envelope first: apply must enforce the same bound, + // even when a client modifies a previously valid plan's embedded request. + let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await; + let status = response.status(); + let mut plan: Value = response.json().await.unwrap(); + assert_eq!(status, StatusCode::OK, "{plan}"); + assert_eq!(plan["plan"]["conflicts"], json!([])); + unchanged(&server, &before, &disk, &bytes).await; + + // Syntactically typed but semantically inapplicable decisions deliberately + // distinguish capacity admission (400) from core semantic rejection (422 or + // a preview containing conflicts). The bound counts entries, not unique IDs. + let decision = CommunicationEndpointDecision { + source, + source_port: CommunicationPortChoice::Reuse { port: source }, + target_port: CommunicationPortChoice::Reuse { port: source }, + }; + *request.pointer_mut(decision_path).unwrap() = json!(vec![decision.clone(); 200]); + let response = post(&server, id, &format!("{route}/preview"), &etag, &request).await; + let status = response.status(); + let body: Value = response.json().await.unwrap(); + assert!( + matches!(status, StatusCode::OK | StatusCode::UNPROCESSABLE_ENTITY), + "Exactly 200 decisions must pass capacity admission: {status} {body}" + ); + unchanged(&server, &before, &disk, &bytes).await; + + let over_limit = json!(vec![decision; 201]); + *request.pointer_mut(decision_path).unwrap() = over_limit.clone(); + *plan.pointer_mut(applied_decision_path).unwrap() = over_limit; + for (operation, body) in [("preview", request), ("apply", plan)] { + let response = post(&server, id, &format!("{route}/{operation}"), &etag, &body).await; + let status = response.status(); + let error: Value = response.json().await.unwrap(); + assert_eq!( + status, + StatusCode::BAD_REQUEST, + "{route}/{operation}: {error}" + ); + assert_eq!(error["code"], "invalid_request", "{error}"); + let detail = error["detail"].as_str().unwrap().to_lowercase(); + assert!( + detail.contains("200") && detail.contains("endpoint"), + "Capacity rejection must identify endpoint decisions: {error}" + ); + unchanged(&server, &before, &disk, &bytes).await; + } + server.stop().await; + let server = TestServer::start(directory.path(), None).await; + unchanged(&server, &before, &disk, &bytes).await; + server.stop().await; +} + +#[tokio::test] +async fn ordinary_transition_endpoint_decision_limit_rejects_201_without_mutation() { + endpoint_limit(false).await; +} + +#[tokio::test] +async fn semantic_transition_endpoint_decision_limit_rejects_201_without_mutation() { + endpoint_limit(true).await; +} diff --git a/crates/oa-server/tests/http/data_contracts.rs b/crates/oa-server/tests/http/data_contracts.rs index bee0aeb8..030e649b 100644 --- a/crates/oa-server/tests/http/data_contracts.rs +++ b/crates/oa-server/tests/http/data_contracts.rs @@ -185,7 +185,7 @@ async fn published_data_contract_queries_atomic_edits_identity_and_restart() { .text() .await .unwrap(); - assert!(source.starts_with("arcadia 5;")); + assert!(source.starts_with("arcadia 6;")); assert_eq!(oa_core::language::parse(&source).unwrap(), removed.model); server.stop().await; let server = TestServer::start(directory.path(), None).await; @@ -218,7 +218,10 @@ async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_fil let resource = summary["resource_id"].as_str().unwrap().parse().unwrap(); let stored = server.get(resource).await; let report = stored.migration.as_ref().unwrap(); - assert_eq!((report.from_schema, report.to_schema), (3, 5)); + assert_eq!( + (report.from_schema, report.to_schema), + (3, oa_core::SCHEMA_VERSION) + ); assert_eq!(report.generated_interface_allocations.len(), 1); assert!(!report.semantic_defaults.is_empty()); let validated: Value = server @@ -241,7 +244,7 @@ async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_fil 1 ); assert_eq!(stored.source_project.files.len(), 2); - assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 5;")); + assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 6;")); for text in [ "// Event rationale", "// Interface rationale", @@ -283,9 +286,9 @@ async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_sav let server = TestServer::start(directory.path(), None).await; let opened = server.get(resource).await; assert_eq!(opened.revision, 17); - assert_eq!(opened.model.schema_version, 5); + assert_eq!(opened.model.schema_version, oa_core::SCHEMA_VERSION); assert_eq!(opened.migration.as_ref().unwrap().from_schema, 3); - assert!(opened.source_project.files["project.arcadia"].contains("arcadia 5;")); + assert!(opened.source_project.files["project.arcadia"].contains("arcadia 6;")); assert_pair(&opened); assert_eq!(std::fs::read(&filename).unwrap(), original); assert_eq!( @@ -717,7 +720,7 @@ async fn qualified_call_reply_values_reject_invalid_edits_and_survive_identity_s .text() .await .unwrap(); - assert!(source.starts_with("arcadia 5;")); + assert!(source.starts_with("arcadia 6;")); assert_eq!(oa_core::language::parse(&source).unwrap(), changed.model); let saved = std::fs::read(&filename).unwrap(); server.stop().await; diff --git a/crates/oa-server/tests/http/source6_views.rs b/crates/oa-server/tests/http/source6_views.rs new file mode 100644 index 00000000..59fafcf2 --- /dev/null +++ b/crates/oa-server/tests/http/source6_views.rs @@ -0,0 +1,374 @@ +use super::*; +use oa_core::{ + ComponentPortKind, ComponentPortOrientation, Position, ProjectionKind, RelationBody, + SharedBody, + source_project::{ProjectLimits, SourceBundle, compile_bundle}, + transitions::{ + CommunicationEndpointDecision, CommunicationPortChoice, TransitionDirection, + TransitionMapping, TransitionRequest, + }, + view_options::{ + CategorySelection, EdgeKind, ExchangeFamily, ExchangeLabels, RelationKinds, ViewOptions, + }, +}; +use std::collections::BTreeMap; + +struct Fixture { + bundle: SourceBundle, + mean: Id, + category: Id, + diagram: Id, + item: Id, + oa: [Id; 2], + sa: [Id; 2], +} + +fn fixture() -> Fixture { + let mut model = Project::empty(); + let a = model + .add_element(Kind::Entity, Layer::Oa, "Rescue team", None) + .unwrap(); + let b = model + .add_element(Kind::Actor, Layer::Oa, "Control centre", None) + .unwrap(); + let sa = model + .add_element(Kind::System, Layer::Sa, "Radio", None) + .unwrap(); + let sb = model + .add_element(Kind::SystemActor, Layer::Sa, "External radio", None) + .unwrap(); + let item = model + .add_element(Kind::ExchangeItem, Layer::Oa, "Position & status", None) + .unwrap(); + let mean = model.relate(RelationKind::CommunicationMean, "Radio ", a, b); + let category = model + .add_element(Kind::ExchangeCategory, Layer::Oa, "Radio channels", None) + .unwrap(); + *model + .elements + .get_mut(&category) + .unwrap() + .shared_mut() + .unwrap() = SharedBody::ExchangeCategory { + family: ExchangeFamily::Participant, + exchanges: vec![mean], + }; + let diagram = model.ensure_diagram(Layer::Oa); + model.place(diagram, a, Position { x: 0., y: 0. }); + model.place(diagram, b, Position { x: 400., y: 0. }); + let view = model.diagrams.get_mut(&diagram).unwrap(); + view.projection = ProjectionKind::Allocation; + view.view_options = ViewOptions { + relation_kinds: RelationKinds::Only(vec![EdgeKind::Relation( + RelationKind::CommunicationMean, + )]), + participant_categories: CategorySelection::Any(vec![category]), + exchange_labels: ExchangeLabels::NameAndItems, + ..Default::default() + }; + let source = format!( + "// Authored source-6 engineering rationale\n{}", + oa_core::language::format(&model).replacen("{\n", "{\n include \"notes.arcadia\";\n", 1) + ); + let bundle = SourceBundle { + entry: "radio.arcadia".into(), + files: BTreeMap::from([ + ("radio.arcadia".into(), source), + ( + "notes.arcadia".into(), + "// Retain this exact included note, including CRLF.\r\n".into(), + ), + ]), + }; + let compiled = compile_bundle(&bundle, ProjectLimits::default()).unwrap(); + assert_eq!(compiled.decoded.project, model); + Fixture { + bundle: compiled.bundle, + mean, + category, + diagram, + item, + oa: [a, b], + sa: [sa, sb], + } +} + +async fn import(server: &TestServer, bundle: &SourceBundle) -> StoredProject { + let response = server + .client + .post(server.url("/projects/import-source-project")) + .json(bundle) + .send() + .await + .unwrap(); + let status = response.status(); + let result: Value = response.json().await.unwrap(); + assert_eq!(status, StatusCode::CREATED, "{result}"); + server + .get(result["resource_id"].as_str().unwrap().parse().unwrap()) + .await +} + +fn assert_pair(stored: &StoredProject) { + let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap(); + assert_eq!(compiled.bundle, stored.source_project); + assert_eq!(compiled.decoded.project, stored.model); + assert_eq!(stored.model.schema_version, 6); +} + +async fn transact( + server: &TestServer, + current: &StoredProject, + commands: Vec, +) -> reqwest::Response { + server + .client + .post(server.url(&format!("/projects/{}/transactions", current.resource_id))) + .header("if-match", current.etag()) + .json(&json!({"commands":commands})) + .send() + .await + .unwrap() +} + +#[tokio::test] +async fn source6_http_views_categories_and_means_preserve_source_and_reject_invalid_batch_after_restart() + { + let f = fixture(); + let directory = tempfile::tempdir().unwrap(); + let server = TestServer::start(directory.path(), None).await; + let original = import(&server, &f.bundle).await; + let resource = original.resource_id; + assert_eq!(original.source_project, f.bundle); + assert_pair(&original); + let model: Value = server + .client + .get(server.url(&format!("/projects/{resource}/model"))) + .send() + .await + .unwrap() + .error_for_status() + .unwrap() + .json() + .await + .unwrap(); + assert_eq!( + model["relations"][f.mean.to_string()]["body"]["CommunicationMean"]["oriented"], + false + ); + assert_eq!( + model["diagrams"][f.diagram.to_string()]["view_options"]["exchange_labels"], + "NameAndItems" + ); + for (route, expected) in [ + ("relations?kind=CommunicationMean", f.mean), + ("elements?kind=ExchangeCategory", f.category), + ] { + let result: Value = server + .client + .get(server.url(&format!("/projects/{resource}/{route}"))) + .send() + .await + .unwrap() + .error_for_status() + .unwrap() + .json() + .await + .unwrap(); + assert_eq!(result["items"].as_array().unwrap().len(), 1); + assert_eq!(result["items"][0]["id"], expected.to_string()); + } + // There is no projection HTTP route. The actual returned model supplies the + // shared desktop projection; saved filters must not alter queried relations. + assert!( + oa_core::projection::project_view(&original.model, f.diagram) + .edges + .iter() + .any(|e| e.id == f.mean) + ); + let mut relation = original.model.relations[&f.mean].clone(); + if let RelationBody::CommunicationMean { + oriented, items, .. + } = &mut relation.body + { + *oriented = true; + *items = vec![f.item]; + } + let mut diagram = original.model.diagrams[&f.diagram].clone(); + diagram.view_options.hidden_edges = vec![f.mean]; + diagram.view_options.exchange_labels = ExchangeLabels::Items; + let response = transact( + &server, + &original, + vec![ + Command::EditRelation(f.mean, relation.clone()), + Command::EditDiagram(f.diagram, diagram.clone()), + ], + ) + .await; + let status = response.status(); + let result: Value = response.json().await.unwrap(); + assert_eq!(status, StatusCode::OK, "{result}"); + let changed = server.get(resource).await; + assert_eq!(changed.revision, original.revision + 1); + assert_eq!(changed.model.relations[&f.mean], relation); + assert_eq!(changed.model.diagrams[&f.diagram], diagram); + assert!( + oa_core::projection::project_view(&changed.model, f.diagram) + .edges + .is_empty() + ); + assert_eq!( + changed.source_project.files["notes.arcadia"], + f.bundle.files["notes.arcadia"] + ); + assert!( + changed.source_project.files["radio.arcadia"] + .starts_with("// Authored source-6 engineering rationale\n") + ); + assert_pair(&changed); + let disk = directory.path().join(format!("{resource}.json")); + let accepted_bytes = std::fs::read(&disk).unwrap(); + let mut invalid = changed.model.elements[&f.category].clone(); + if let Some(SharedBody::ExchangeCategory { family, .. }) = invalid.shared_mut() { + *family = ExchangeFamily::Work; // A communication means cannot join this family. + } + relation.name = "Must not leak from rejected batch".into(); + let rejected = transact( + &server, + &changed, + vec![ + Command::EditRelation(f.mean, relation), + Command::EditElement(f.category, invalid), + ], + ) + .await; + assert_eq!(rejected.status(), StatusCode::UNPROCESSABLE_ENTITY); + let retained = server.get(resource).await; + assert_eq!(retained.model, changed.model); + assert_eq!(retained.source_project, changed.source_project); + assert_eq!(retained.etag(), changed.etag()); + assert_eq!(std::fs::read(&disk).unwrap(), accepted_bytes); + server.stop().await; + let server = TestServer::start(directory.path(), None).await; + let reopened = server.get(resource).await; + assert_eq!(reopened.model, changed.model); + assert_eq!(reopened.source_project, changed.source_project); + assert_eq!(reopened.etag(), changed.etag()); + assert_pair(&reopened); + server.stop().await; +} + +#[tokio::test] +async fn source6_http_explicit_communication_ports_preview_apply_and_restart_preserve_decisions() { + let f = fixture(); + let directory = tempfile::tempdir().unwrap(); + let server = TestServer::start(directory.path(), None).await; + let original = import(&server, &f.bundle).await; + let resource = original.resource_id; + let mut request = TransitionRequest::new(TransitionDirection::OaToSa, vec![f.mean]); + request.mappings = + f.oa.into_iter() + .zip(f.sa) + .map(|(source, target)| TransitionMapping { + source, + target: Some(target), + target_kind: None, + }) + .collect(); + let ports = [Id::new_v4(), Id::new_v4()]; + let create = |id, alias: &str| CommunicationPortChoice::Create { + id, + alias: alias.into(), + name: alias.into(), + description: "Explicit HTTP endpoint choice".into(), + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }; + request + .communication_endpoints + .push(CommunicationEndpointDecision { + source: f.mean, + source_port: create(ports[0], "http_radio_source"), + target_port: create(ports[1], "http_radio_target"), + }); + let accepted_bytes = std::fs::read(directory.path().join(format!("{resource}.json"))).unwrap(); + let response = server + .client + .post(server.url(&format!("/projects/{resource}/transitions/preview"))) + .header("if-match", original.etag()) + .json(&request) + .send() + .await + .unwrap(); + let status = response.status(); + let preview: Value = response.json().await.unwrap(); + assert_eq!(status, StatusCode::OK, "{preview}"); + assert!( + preview["plan"]["conflicts"].as_array().unwrap().is_empty(), + "{preview}" + ); + assert_eq!( + preview["plan"]["request"]["communication_endpoints"], + serde_json::to_value(&request.communication_endpoints).unwrap() + ); + assert_eq!( + server.get(resource).await.source_project, + original.source_project + ); + assert_eq!( + std::fs::read(directory.path().join(format!("{resource}.json"))).unwrap(), + accepted_bytes + ); + let response = server + .client + .post(server.url(&format!("/projects/{resource}/transitions/apply"))) + .header("if-match", original.etag()) + .json(&preview) + .send() + .await + .unwrap(); + let status = response.status(); + let result: Value = response.json().await.unwrap(); + assert_eq!(status, StatusCode::OK, "{result}"); + let changed = server.get(resource).await; + assert_eq!(changed.revision, original.revision + 1); + assert_pair(&changed); + let target = changed + .model + .traces + .values() + .find(|t| t.oa == f.mean) + .unwrap() + .sa; + assert!( + matches!(changed.model.relations[&target].body, RelationBody::BoundaryExchange { source, target, oriented:false, .. } if [source,target] == ports) + ); + for (port, owner) in ports.into_iter().zip(f.sa) { + assert_eq!(changed.model.elements[&port].parent, Some(owner)); + } + assert_eq!( + changed.model.relations[&f.mean], + original.model.relations[&f.mean] + ); + assert_eq!( + changed.model.elements[&f.category], + original.model.elements[&f.category] + ); + assert_eq!( + changed.model.diagrams[&f.diagram], + original.model.diagrams[&f.diagram] + ); + assert_eq!( + changed.source_project.files["notes.arcadia"], + original.source_project.files["notes.arcadia"] + ); + server.stop().await; + let server = TestServer::start(directory.path(), None).await; + let reopened = server.get(resource).await; + assert_eq!(reopened.model, changed.model); + assert_eq!(reopened.source_project, changed.source_project); + assert_eq!(reopened.etag(), changed.etag()); + assert_pair(&reopened); + server.stop().await; +} diff --git a/crates/oa-server/tests/semantic_schema.py b/crates/oa-server/tests/semantic_schema.py index 380974be..d5e5b56f 100644 --- a/crates/oa-server/tests/semantic_schema.py +++ b/crates/oa-server/tests/semantic_schema.py @@ -1,6 +1,7 @@ """Refresh the closed semantic OpenAPI catalog from the frozen Rust leaf table. -Run from any directory with Python 3: semantic_schema.py [--check]. +Run from any directory with Python 3: semantic_schema.py [--check] [--validate]. +--validate additionally runs source-6 structural fixtures with jsonschema. This generates documentation only, never runtime selectors or model mutations. """ import copy @@ -15,6 +16,7 @@ spec = json.loads(PATH.read_text(encoding="utf-8")) schemas = spec["components"]["schemas"] types = (ROOT / "crates/oa-core/src/semantic/types.rs").read_text(encoding="utf-8") model = (ROOT / "crates/oa-core/src/model.rs").read_text(encoding="utf-8") +views = (ROOT / "crates/oa-core/src/view_options.rs").read_text(encoding="utf-8") def ref(name): @@ -47,6 +49,7 @@ def variant(name, value): def typed(name): + name = name.replace("crate::view_options::", "") if name.startswith("Option<"): return optional(typed(name[7:-1])) if name.startswith("Vec<"): @@ -59,6 +62,7 @@ def typed(name): def reviewed(name): + name = name.replace("crate::view_options::", "") key = "Reviewed" + re.sub(r"[^A-Za-z0-9]", "", name) observation = {"oneOf": [enum(["NotApplicable"]), variant("Value", typed(name))]} schemas[key] = obj({"oa": observation, "sa": copy.deepcopy(observation)}) @@ -74,8 +78,9 @@ def keypair(name): for name in ["DataConstructor", "PortConstructor", "OccurrenceConstructor", "EventConstructor", "MessageConstructor", "ReferenceCollection", "OrderedCollection", "IncidentEnd", "ArtifactFacet", "ComponentPortKind", "ComponentPortOrientation", "PortDirection", - "FragmentOperator", "StateActionKind", "ArtifactRelation"]: - source = types if f"pub enum {name}" in types else model + "FragmentOperator", "StateActionKind", "ArtifactRelation", "ExchangeFamily", + "Coverage", "ExchangeLabels", "Kind", "RelationKind"]: + source = next(s for s in [types, model, views] if f"pub enum {name}" in s) body = re.search(r"pub enum " + name + r"\s*\{([^}]+)\}", source).group(1) body = re.sub(r"//[^\n]*|#\[[^\]]*\]", "", body) names = [value.strip() for value in body.split(",") if value.strip()] @@ -84,17 +89,17 @@ for name in ["DataConstructor", "PortConstructor", "OccurrenceConstructor", "Eve catalog = types.split("macro_rules! scalar_families", 1)[1].split("pub(super) use", 1)[0] families = re.findall(r"(\w+), (\w+), (\w+) \{([^}]+)\}", catalog) -assert len(families) == 13 +assert len(families) == 14 selectors, reviews = [], [] leaf_count = 0 for family, selector, review, body in families: - leaves = re.findall(r"(\w+): ([\w<>]+)", body) + leaves = re.findall(r"(\w+): ([\w:<>]+)", body) leaf_count += len(leaves) schemas[selector] = enum([leaf for leaf, _ in leaves]) schemas[review] = {"oneOf": [variant(leaf, reviewed(value)) for leaf, value in leaves]} selectors.append(variant(family, ref(selector))) reviews.append(variant(family, ref(review))) -assert leaf_count == 84 +assert leaf_count == 85 schemas["AnnotationKey"] = obj({"path": array(ref("Id")), "field": ref("Id")}) id_keys, text_keys, annotation_keys = keypair("Id"), keypair("String"), keypair("AnnotationKey") artifact_types = {"Presence": "bool", "Name": "String", "Description": "String", "Owner": "Id", @@ -119,13 +124,49 @@ schemas["FacetReview"] = {"oneOf": reviews} schemas["SemanticBaseline"] = obj({"facets": array(ref("FacetReview"))}) schemas["TraceBaseline"]["properties"]["semantic"] = {**optional(ref("SemanticBaseline")), "default": None, "description": "Absent/null means Unreviewed. Historical typed facts never become live graph references."} -schemas["Project"]["properties"]["schema_version"]["const"] = 5 -schemas["MigrationReport"]["properties"]["from_schema"]["enum"] = [1, 2, 3, 4] -schemas["MigrationReport"]["properties"]["to_schema"]["const"] = 5 -schemas["MigrationReport"]["description"] = schemas["MigrationReport"]["description"].replace("V1/V2/V3 to V4", "V1/V2/V3/V4 to V5") -spec["info"]["description"] = spec["info"]["description"].replace("schema-4", "schema-5") -spec["paths"]["/api/v1/projects/import"]["post"]["description"] = "Schema-5 native JSON is described below. Schema-1/2/3/4 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Prior prose reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 5 / arcadia 5. See docs/language.rst." -schemas["SourceBundle"]["description"] = schemas["SourceBundle"]["description"].replace("source4 workspace (source3 migrates explicitly)", "source5 workspace (source3/4 migrate explicitly)") +schemas["Project"]["properties"]["schema_version"]["const"] = 6 +schemas["MigrationReport"]["properties"]["from_schema"]["enum"] = [1, 2, 3, 4, 5] +schemas["MigrationReport"]["properties"]["to_schema"]["const"] = 6 +schemas["MigrationReport"]["description"] = re.sub(r"V1(?:/V[2-5])+ to V[4-6]", "V1/V2/V3/V4/V5 to V6", schemas["MigrationReport"]["description"]) +spec["info"]["description"] = re.sub(r"schema-[45]", "schema-6", spec["info"]["description"]) +spec["paths"]["/api/v1/projects/import"]["post"]["description"] = "Schema-6 native JSON is described below. Schema-1/2/3/4/5 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Earlier versions reject source-6 constructors and saved view fields. Prior prose and typed reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 6 / arcadia 6. See docs/language.rst." +schemas["SourceBundle"]["description"] = re.sub(r"source[45] workspace \(source[^)]+\)", "source6 workspace (source3/4/5 migrate explicitly)", schemas["SourceBundle"]["description"]) + +# Source-6 additions are closed serde records; selectors with an empty payload +# remain explicit selections rather than reverting to their omitted defaults. +schemas["EdgeKind"] = {"oneOf": [enum(["Trace"]), variant("Relation", ref("RelationKind"))]} +schemas["RelationKinds"] = {"oneOf": [enum(["All"]), variant("Only", array(ref("EdgeKind")))]} +for selection in ["CategorySelection", "ContextSelection"]: + schemas[selection] = {"oneOf": [enum(["All"]), variant("Any", array(ref("Id")))]} +schemas["ViewOptions"] = obj({ + "hidden_edges": {**array(ref("Id")), "default": []}, + "relation_kinds": {**ref("RelationKinds"), "default": "All"}, + "work_categories": {**ref("CategorySelection"), "default": "All"}, + "participant_categories": {**ref("CategorySelection"), "default": "All"}, + "coverage": {**ref("Coverage"), "default": "All"}, + "work_contexts": {**ref("ContextSelection"), "default": "All"}, + "exchange_labels": {**ref("ExchangeLabels"), "default": "Name"}}, []) +schemas["ViewOptions"]["description"] = "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. Traceability permits both-layer hidden/category/context references; ordinary projections require the diagram layer. Filtering preserves unfiltered automatic port positions." +schemas["Diagram"]["properties"]["view_options"] = {**ref("ViewOptions"), "default": {}} + +def upsert_variant(variants, name, value): + variants[:] = [v for v in variants if name not in v.get("properties", {})] + variants.append(variant(name, value)) + +upsert_variant(schemas["RelationBody"]["oneOf"], "CommunicationMean", obj({ + "source": ref("Id"), "target": ref("Id"), "oriented": typed("bool"), "items": array(ref("Id"))})) +upsert_variant(schemas["SharedBody"]["oneOf"][1]["oneOf"], "ExchangeCategory", obj({ + "family": ref("ExchangeFamily"), "exchanges": array(ref("Id"))})) +schemas["CommunicationPortChoice"] = {"oneOf": [ + variant("Reuse", obj({"port": ref("Id")})), + variant("Create", obj({"id": ref("Id"), "alias": typed("String"), "name": typed("String"), + "description": typed("String"), "kind": ref("ComponentPortKind"), + "orientation": ref("ComponentPortOrientation")}))]} +schemas["CommunicationEndpointDecision"] = obj({"source": ref("Id"), + "source_port": ref("CommunicationPortChoice"), "target_port": ref("CommunicationPortChoice")}) +schemas["TransitionRequest"]["properties"]["communication_endpoints"] = { + **array(ref("CommunicationEndpointDecision"), 200), "default": [], + "description": "Explicit SA component-port pair per selected OA CommunicationMean. Reuse or Create only. Owners follow reviewed participant mappings; no participant-to-port substitution or automatic interface allocation."} schemas["Drift"] = enum(["Unreviewed", "Unchanged", "OaChanged", "SaChanged", "BothChanged"]) schemas["FacetDrift"] = obj({"selector": ref("FacetSelector"), "current": ref("FacetReview"), "drift": ref("Drift")}) schemas["SemanticDrift"] = obj({"own": ref("Drift"), "linked": ref("Drift"), @@ -197,6 +238,70 @@ def check_refs(value): check_refs(spec) + +if "--validate" in sys.argv: + from jsonschema import Draft202012Validator, FormatChecker + + for schema in schemas.values(): + Draft202012Validator.check_schema(schema) + + fixture_count = 0 + + def accepts(name, value, expected=True): + global fixture_count + validator = Draft202012Validator({**ref(name), "components": spec["components"]}, + format_checker=FormatChecker()) + errors = list(validator.iter_errors(value)) + assert (not errors) == expected, (name, value, [e.message for e in errors]) + fixture_count += 1 + + a, b = "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + mean = {"source": a, "target": b, "oriented": False, "items": []} + accepts("RelationBody", {"CommunicationMean": mean}) + accepts("RelationBody", {"CommunicationMean": {**mean, "typo": True}}, False) + for field in mean: + accepts("RelationBody", {"CommunicationMean": {k: v for k, v in mean.items() if k != field}}, False) + category = {"family": "Participant", "exchanges": [a]} + accepts("SharedBody", {"ExchangeCategory": category}) + accepts("SharedBody", {"ExchangeCategory": {**category, "family": "Future"}}, False) + accepts("SharedBody", {"ExchangeCategory": {**category, "typo": []}}, False) + accepts("ViewOptions", {}) + options = {"hidden_edges": [a], "relation_kinds": {"Only": ["Trace", {"Relation": "CommunicationMean"}]}, + "work_categories": {"Any": []}, "participant_categories": {"Any": [b]}, + "coverage": "OutsideBehavior", "work_contexts": {"Any": []}, "exchange_labels": "NameAndItems"} + accepts("ViewOptions", options) + accepts("ViewOptions", {**options, "relation_kinds": {"Only": []}}) + for field, value in [("typo", []), ("coverage", "Unknown"), ("hidden_edges", ["not-an-id"]), + ("relation_kinds", {"Only": [], "typo": []}), + ("relation_kinds", {"Only": [{"Relation": "Future"}]}), + ("work_contexts", {"Any": [], "typo": []}), + ("exchange_labels", "Names")]: + accepts("ViewOptions", {**options, field: value}, False) + create = {"id": a, "alias": "radio", "name": "Radio", "description": "", + "kind": "Standard", "orientation": "Unset"} + accepts("CommunicationPortChoice", {"Create": create}) + accepts("CommunicationPortChoice", {"Reuse": {"port": a}}) + accepts("CommunicationPortChoice", "Auto", False) + accepts("CommunicationPortChoice", {"Reuse": {"port": a, "owner": b}}, False) + for field in create: + accepts("CommunicationPortChoice", {"Create": {k: v for k, v in create.items() if k != field}}, False) + pair = {"source": a, "source_port": {"Create": create}, "target_port": {"Reuse": {"port": b}}} + accepts("CommunicationEndpointDecision", pair) + accepts("CommunicationEndpointDecision", {k: v for k, v in pair.items() if k != "target_port"}, False) + request = {"direction": "OaToSa", "sources": [a], "scope": "SelectedOnly", "mappings": [], + "fields": [], "resolutions": [], "destination_diagram": None} + accepts("TransitionRequest", request) + accepts("TransitionRequest", {**request, "communication_endpoints": [pair]}) + accepts("TransitionRequest", {**request, "communication_endpoints": [pair] * 200}) + accepts("TransitionRequest", {**request, "communication_endpoints": [pair] * 201}, False) + accepts("FacetSelector", {"Category": "Family"}) + accepts("FacetSelector", {"Category": "Unknown"}, False) + accepts("FacetReview", {"Category": {"Family": {"oa": {"Value": "Work"}, "sa": {"Value": "Participant"}}}}) + accepts("FacetSelector", {"Member": {"collection": "CategoryExchanges", "keys": {"oa": a, "sa": b}}}) + assert schemas["Project"]["properties"]["schema_version"]["const"] == 6 + assert schemas["MigrationReport"]["properties"]["from_schema"]["enum"] == [1, 2, 3, 4, 5] + print(f"Source-6 schema fixtures: {fixture_count} passed; all {len(schemas)} component schemas well-formed") + rendered = json.dumps(spec, indent=2, ensure_ascii=False) + "\n" if "--check" in sys.argv: assert json.loads(PATH.read_text(encoding="utf-8")) == spec, "Run semantic_schema.py to refresh OpenAPI" diff --git a/docs/api.rst b/docs/api.rst index 1d3492e6..2e3fdbbc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -155,8 +155,8 @@ Parsing, validation and query processing use snapshot workers, so this work does not hold the catalog lock or serialize unrelated model preparation. Import accepts schema-1 JSON through the shared explicit migration adapter, -as well as schema-2, schema-3, schema-4 and current schema-5 JSON. Source import uses the shared language parser -and its supported migrations. Export uses schema 5 / arcadia 5. Native JSON +as well as schema-2, schema-3, schema-4, schema-5 and current schema-6 JSON. Source import uses the shared language parser +and its supported migrations. Export uses schema 6 / arcadia 6. Native JSON replacement accepts supported older schemas through the same explicit migration. Only documented query parameters are accepted. In particular, appending ``?revision=...`` to model/source export is an error, not historical retrieval. @@ -169,7 +169,7 @@ A source bundle contains ``entry`` and a map of ``files``. For example:: { "entry": "rescue.arcadia", "files": { - "rescue.arcadia": "arcadia 5; project rescue \"Rescue\" { include \"oa/work.arcadia\"; }\n", + "rescue.arcadia": "arcadia 6; project rescue \"Rescue\" { include \"oa/work.arcadia\"; }\n", "oa/work.arcadia": "// Engineering rationale\noa_activity assess \"Assess assistance\" {}\n" } } @@ -181,7 +181,7 @@ targets or another hosted resource. All submitted files must be reachable; missing files, cycles, duplicate keys, case collisions and escaping paths are rejected. The bundle permits at most 256 files, 32 include edges along a path, and 1,024 include edges in total. Model and source-byte budgets apply across -the complete bundle, not separately per file. Current bundle syntax is source5; source3/4 bundles migrate explicitly while +the complete bundle, not separately per file. Current bundle syntax is source6; source3/4/5 bundles migrate explicitly while preserving their files/includes/comments. V1/V2 single-file migration remains available through interchange import. @@ -403,7 +403,7 @@ contract in the same transaction when removing a used interface allocation. Explicit schema1/2/3 migration preserves prior identities and typed ports. Reports add semantic defaults, generated interface allocations and unresolved message values. Old source3 bundles retain files/includes/comments during upgrade; all -current exports use schema5/source5. Reading an old envelope does not write it or +current exports use schema6/source6. Reading an old envelope does not write it or advance its ETag. Canonical exports omit transient migration reports. Validation @@ -419,7 +419,7 @@ or:: or:: - {"format":"source-project", "source_project":{"entry":"rescue.arcadia", "files":{"rescue.arcadia":"arcadia 5; project rescue \"Rescue\" {}"}}} + {"format":"source-project", "source_project":{"entry":"rescue.arcadia", "files":{"rescue.arcadia":"arcadia 6; project rescue \"Rescue\" {}"}}} A well-formed validation request returns 200 with ``valid`` and ``issues`` even if its candidate is invalid, including a model record that fails typed decoding. @@ -491,17 +491,63 @@ Other projections continue to preserve hidden placements. Rejected writes retain the accepted model, source bundle, revision and ETag. Reading a state view has no semantic or persistence side effects. +Source-6 communication and saved views +------------------------------------- + +The same import, replacement and command endpoints admit OA +``CommunicationMean { source, target, oriented, items }`` and root-level +``ExchangeCategory { family, exchanges }`` records. Communication means join +distinct OA entities/actors. Categories group same-layer exchanges as ``Work`` +or ``Participant`` without owning them. Existing legacy OA boundary exchanges +retain their identities and contracts; migration does not silently replace them. + +``Diagram.view_options`` is an optional closed object with defaults: empty +``hidden_edges``, ``All`` for ``relation_kinds``, both category selections, +``coverage`` and ``work_contexts``, and ``Name`` for ``exchange_labels``. +``{"Only":[]}`` hides all relation and trace lines; ``{"Any":[]}`` preserves +an explicit empty category/context selection. Category members union within +each family, and independent filters intersect. ``Traceability`` permits +references from both layers; ordinary projections require the diagram's layer. +These presentation choices preserve semantic records, routes, placements and +the automatic port positions derived from unfiltered eligible exchanges. + +Earlier schema/source versions reject the new constructors, category review +facts and explicit ``view_options`` field, including an empty object. Their +frozen readers migrate accepted content to schema/source 6 with default view +options. The OpenAPI document describes current records; it is not a permissive +union of historical input formats. Source-6 integration and release acceptance +are tracked separately in the release coverage audit. + Cross-layer transitions ----------------------- Preview accepts the shared ``TransitionRequest`` record, including direction (``OaToSa`` or ``SaToOa``), sources, dependency scope, mappings, selected fields, conflict resolutions and optional destination diagram. All fields are explicit -in OpenAPI. At most 100 sources, 200 mappings and 400 field resolutions are +in OpenAPI. At most 100 sources, 200 mappings, 200 communication endpoint +decisions and 400 field resolutions are accepted. A conflict-free plan must fit the same 200-command transaction limit and model/source-capacity limits. A conflict-free preview also checks source writeback, so its commands can update the same accepted authored workspace. +OA ``CommunicationMean`` maps to an SA ``BoundaryExchange`` through an explicit +``communication_endpoints`` array (omitted means empty). Each decision identifies +the source communication means and both ``source_port`` and ``target_port``. +Each port choice is either ``{"Reuse":{"port":"port-uuid"}}`` or +``{"Create":{"id":"new-port-uuid","alias":"radio_port","name":"Radio port", +"description":"","kind":"Standard","orientation":"Unset"}}``. Created or +reused SA component ports must belong to the explicitly mapped participants. +These per-exchange choices do not replace participant correspondence with port +IDs. Missing or incompatible choices remain preview conflicts. Interface and +functional-port allocations use their existing explicit workflows; an engineer +may omit an unresolved allocation from this transition. Reverse transition maps +boundary ports through their owners to OA participants. + +``ExchangeCategory`` transitions preserve their family and map only explicitly +corresponding member exchanges. Missing member mappings are conflicts, not new +fabricated exchanges. Typed review exposes ``Category.Family`` and the +``CategoryExchanges`` member collection; historical review facts remain exact. + The preview response is ``{"base_revision":1,"plan":{...}}``. It contains planned changes, conflicts, warnings and exact commands/IDs. It does not write the model or persist a server-side plan. Apply accepts that entire response and diff --git a/docs/language.rst b/docs/language.rst index 0c114f5c..36c305ed 100644 --- a/docs/language.rst +++ b/docs/language.rst @@ -1,8 +1,8 @@ Arcadia Studio OA / SA Language and Editing Contract ==================================================== -:Version: 5 -:Status: Normative current implementation contract; first-release integration +:Version: 6 (integration in progress) +:Status: Source-6 contract under integration; native and API acceptance pending :Core implementation: ``crates/oa-core`` :Text suffix: ``.arcadia`` @@ -11,10 +11,12 @@ Arcadia Studio OA / SA Language and Editing Contract This specification defines the shared Operational Analysis (OA) and System Analysis (SA) model, its textual language, graphical editing obligations, -validation, persistence, and explicit migration from versions 1, 2, 3 and 4. MUST, MUST NOT, +validation, persistence, and explicit migration from versions 1, 2, 3, 4 and 5. MUST, MUST NOT, and MAY indicate requirements, prohibitions, and permitted choices. -Schema 5 adds closed typed semantic review facts to trace baselines, including +Schema 6 adds OA communication means, exchange categories and saved view options. +Their integration and acceptance are tracked separately; this version label does +not claim completed graphical/API parity. Schema 5 added closed typed semantic review facts to trace baselines, including historical references that may no longer exist in the live model. It retains schema 4 bounded reusable data types, typed defaults, exchange mechanisms, qualified values, specialization, interface allocations and associations, and @@ -41,7 +43,7 @@ Actual verification evidence is separate from this contract. 2. One authoritative model -------------------------- -A Project stores ``schema_version`` (5), ``id``, ``alias``, ``name``, +A Project stores ``schema_version`` (6), ``id``, ``alias``, ``name``, ``description``, and maps named ``elements``, ``relations``, ``diagrams``, ``artifact_links``, and ``traces``. Each map is keyed by the UUID contained in its record. Duplicate map keys and mismatching keys/identities are errors. @@ -246,6 +248,9 @@ references. Their kind is the declaration keyword: * - ``boundary_exchange`` - Different participant owners; typed component ports have matching kinds - ``oriented`` bool / true; ``items`` refs / [] + * - ``communication_mean`` + - Different OA Entity/Actor elements; no operational ports or role inference + - ``oriented`` bool / false; ``items`` refs / [] * - ``port_allocation`` - SA Component port → SA FunctionInput/FunctionOutput port - Requires the function's actual work Allocation to that component @@ -286,7 +291,7 @@ references. Their kind is the declaration keyword: - Work, participant, capability, or process/chain → state/mode - None * - ``exchange_allocation`` - - Boundary-exchange relation → same-layer interaction (OA) or functional exchange (SA) + - CommunicationMean → OA Interaction, or BoundaryExchange → same-layer Interaction/FunctionalExchange - None * - ``interface_provision``, ``interface_requirement`` - Participant/port → provided/required interface @@ -300,6 +305,89 @@ exchanges with distinct identities are permitted. Self-relations are invalid except for a state transition that returns to its source state or an explicit DataAssociation between two ends of the same Record type. +Source-6 communication and exchange categories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An OA ``communication_mean`` joins two distinct Entity/Actor records directly. +Its stable source/target tuple does not create operational ports. An unoriented +means permits either direction; an oriented ordinary use follows source to target. +The empty carried-item set is explicit, not a wildcard. Every item carried by an +allocated Interaction must specialize or equal a declared means item, using the +same inheritance rule as existing exchange allocations. + +``exchange_allocation`` remains a separately identified relation. A means may +allocate several Interactions and an Interaction may have several explicit means. +Allocation compares actual activity-to-participant Allocations, without inferring +responsibility from roles or nesting. Every known performer must fit one complete +permitted endpoint pair. A known contradiction is an error; a missing actual +performer is an incomplete-model warning. Reorienting or reconnecting a means +validates all dependent allocations and scenario uses in one candidate. + +ParticipantInteraction and compatible MixedLegacy scenarios may reference means. +For bidirectional use, explicit sender and receiver lifelines determine one +compatible ordered endpoint pair. Missing or ambiguous bindings do not guess a +direction. A Reply reusing its Call's means reverses the resolved actual Call, +including a reverse Call on an unoriented means. The existing operation-response +exception permits the correlated Reply on an oriented means; this implies no +physical transport property. OA process contributions continue to use Interaction. + +Mapped unoriented SA BoundaryExchange messages use the same explicit lifeline +resolution. Ordinary messages also respect component-port direction; legacy +port boundaries retain their stored direction. A correlated same-channel Reply +still reverses the actual Call, including across directional Flow ports. + +CommunicationMean and SA BoundaryExchange support Exchange traces. OA-to-SA +transition requires a separate component-port choice for each end of each means: +reuse an existing compatible port, or create a port with explicitly chosen +Standard/Flow kind and orientation. Participant correspondence never substitutes +a port UUID for its participant UUID. Interfaces and PortAllocations are not +invented; unresolved technical allocations must be reviewed or omitted from the +selection and authored explicitly. Reverse transition projects SA port owners to +reviewed OA participant counterparts, without copying typed SA ports into OA. +Source/Target review retains the exact participant/port identities in history. +Taking an OA endpoint requires the scoped technical port decision, while Keep +and a valid Candidate retain explicit technical choices. + +``oa_exchange_category`` and ``sa_exchange_category`` are root-level named +semantic elements with ``family`` Work (default) or Participant, and ``exchanges`` +references (default []). Work categories contain Interaction/FunctionalExchange; +Participant categories contain CommunicationMean/BoundaryExchange. Members must +be distinct and in the category's layer; categories never own or merge exchanges. +Category transition requires mapped counterpart members. ``Category.Family`` and +``Member { collection CategoryExchanges; ... }`` are closed typed semantic facts. +Deleting an exchange removes its category membership; deleting a category does +not remove its exchanges. Reidentification preserves all live references and +typed historical reference provenance. + +Each Diagram has optional ``view_options`` (all selections unrestricted, no hidden +edges, Name labels by default). Its closed fields are ``hidden_edges``, +``relation_kinds``, ``work_categories``, ``participant_categories``, ``coverage``, +``work_contexts`` and ``exchange_labels``. Relation kinds use All or Only with +Relation(kind)/Trace entries; Only([]) draws no lines, including traces. Category +selectors use All or Any(category IDs); Any([]) draws none of that exchange family. +Selected categories are unioned within each family; independent restrictions +intersect. Coverage is All/InBehavior/OutsideBehavior; work contexts are All or +Any(explicit process, chain or scenario IDs). Contexts follow ordered owned +contributions and explicit reuse, sharing the advisory diagnostic evidence walk. +Labels are Name, Items or NameAndItems. These settings alter presentation only; +automatic port positions use unfiltered eligible lines. Hidden routes and semantic +identities remain saved. Removing a referenced selection preserves explicit empty +filters rather than widening them to All. + +Traceability views retain their established two-layer projection: default options +preserve eligible OA and SA lines. Their hidden-edge, category and context +references may therefore select either layer. All ordinary projections require +these references in the declared diagram layer. A filter never hides the other +layer merely because the Traceability diagram has an OA or SA layer label. + +Versions 1–5 reject the new relation, category constructor, diagram field (even +an explicitly supplied default), and category historical facts. Reading version 5 +preserves existing records, legacy OA boundaries and reviewed facts, with an +explicit upgrade report to version 6. Source upgrading changes the version token +without rewriting unrelated authored trivia. No old boundary is automatically +converted into communication means. Source-6 graphical/API/native acceptance is +still tracked separately from this integration contract. + Direct Allocation has at most one performer per leaf. Role sharing is a different relationship: several OA participants can realize a Role. A direct allocation inconsistent with its role's realized performers produces an @@ -356,7 +444,7 @@ Port deletion cascades to incident FEs; FE deletion retains independent ports. For example:: - arcadia 5; + arcadia 6; project ports_example "Typed function contract" { sa_function send "Send" {} sa_function receive "Receive" {} @@ -739,7 +827,7 @@ This complete data contract example is also exercised through the REST API: .. code-block:: text - arcadia 5; + arcadia 6; project engineering_data "Engineering data" { sa_data_type voltage "Voltage" { definition { Real { minimum 0.0; maximum 48.0; }; }; @@ -808,7 +896,7 @@ This complete source example places one state from an existing machine: .. code-block:: text - arcadia 5; + arcadia 6; project operating "Operating modes" { oa_entity crew "Crew" {} oa_state_machine modes "Modes" { parent @crew; } @@ -1262,7 +1350,7 @@ change. This valid source records deliberately different constructor/unit observations. Its historical integer minimum is absent; the Real side has no integer branch:: - arcadia 5; + arcadia 6; project reviewed_dimensions "Reviewed dimensions" { oa_data_type oa_dimension "Operational distance" { definition { Integer { minimum 0; maximum 10; }; }; @@ -1421,7 +1509,7 @@ declarations and include statements; it MUST NOT contain another version header, project wrapper, or project description. Includes MUST occur at document level, never inside an element body or typed field value. For example:: - arcadia 5; + arcadia 6; project rescue "Rescue" id "00000000-0000-4000-8000-000000000001" { description "Operational model"; include "oa/participants.arcadia"; @@ -1778,7 +1866,7 @@ explicitly before Apply succeeds. Loading detects JSON from its first non-whitespace ``{``; other input is parsed as Arcadia source regardless of filename. Saving an ``.arcadia`` suffix uses -the accepted V5 source bundle (``arcadia 5``); other suffixes use V5 JSON. Both forms preserve the same +the accepted V6 source bundle (``arcadia 6``); other suffixes use V6 JSON. Both forms preserve the same complete model. JSON rejects unknown fields throughout typed payloads, duplicate maps, unsupported versions, and structural errors. @@ -1814,7 +1902,7 @@ order) and ``unresolved_message_values`` (occurrence, field, reason). These new report arrays default empty when reading older responses. Version 1 normalizes through V2, then the typed-port V3 adapter, shared V4 data upgrade and V5 review adapter. Original V2 and real V3 fixtures remain unchanged compatibility inputs. Current exports -always use version 5; reading/migrating alone never saves or changes revisions. +always use version 6; reading/migrating alone never saves or changes revisions. Native source Save All checks the original contents of observed dependencies before staging writes. Existing target files are replaced individually and the @@ -1903,7 +1991,7 @@ the assigned UUIDs explicitly. .. code-block:: text - arcadia 5; + arcadia 6; project harbor "Harbor assistance" { oa_entity control "Harbor control" {} oa_activity assess "Assess assistance" {} diff --git a/docs/openapi.json b/docs/openapi.json index 08d1a073..07832747 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "Arcadia Studio model API", "version": "1.0.0", - "description": "Current schema-5 snapshots and conditional atomic writes. Request body limit: 8 MiB; at most 200 nested commands; no retained revisions, request receipts, automatic retries or persistent transition plans. See docs/api.rst. Fixed store capacity: 128 models / 256 MiB serialized envelopes. Stateless transition preview/apply. Artifact URI/revision strings remain opaque core fields. Model admission before structural validation: 5,000 graph objects, 20,000 total records and 50,000 ordered references. Preparation uses snapshots outside the catalog lock; final commits recheck the exact ETag. Each accepted envelope couples model and authored source under one revision. Source-only edits advance the shared ETag. Source bundles resolve logical paths only, preserve authored files/comments, and have a stabilized JSON cap of 8 MiB, 256 files, 32 include edges along a path and 1,024 include edges overall." + "description": "Current schema-6 snapshots and conditional atomic writes. Request body limit: 8 MiB; at most 200 nested commands; no retained revisions, request receipts, automatic retries or persistent transition plans. See docs/api.rst. Fixed store capacity: 128 models / 256 MiB serialized envelopes. Stateless transition preview/apply. Artifact URI/revision strings remain opaque core fields. Model admission before structural validation: 5,000 graph objects, 20,000 total records and 50,000 ordered references. Preparation uses snapshots outside the catalog lock; final commits recheck the exact ETag. Each accepted envelope couples model and authored source under one revision. Source-only edits advance the shared ETag. Source bundles resolve logical paths only, preserve authored files/comments, and have a stabilized JSON cap of 8 MiB, 256 files, 32 include edges along a path and 1,024 include edges overall." }, "servers": [ { @@ -389,7 +389,7 @@ } } }, - "description": "Schema-5 native JSON is described below. Schema-1/2/3/4 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Prior prose reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 5 / arcadia 5. See docs/language.rst." + "description": "Schema-6 native JSON is described below. Schema-1/2/3/4/5 JSON and source are accepted through explicit frozen readers and migration, also on replacement and candidate validation. Earlier versions reject source-6 constructors and saved view fields. Prior prose and typed reviews remain intact; absent semantic review stays Unreviewed. Export emits schema 6 / arcadia 6. See docs/language.rst." } }, "/api/v1/projects/{id}": { @@ -2039,6 +2039,7 @@ "Mission", "FunctionalChain", "ExchangeItem", + "ExchangeCategory", "DataType", "DataField", "Interface", @@ -2068,7 +2069,10 @@ "RelationKind": { "type": "string", "enum": [ + "InterfaceItemAllocation", + "DataAssociation", "Allocation", + "PortAllocation", "Interaction", "CapabilityInvolvement", "ChainInvolvement", @@ -2076,21 +2080,19 @@ "RoleAssignment", "FunctionalExchange", "BoundaryExchange", + "CommunicationMean", "MissionCapability", - "StateTransition", - "Availability", - "ExchangeAllocation", - "InterfaceProvision", - "InterfaceRequirement", - "StateAction", "CapabilityInclude", "CapabilityExtend", "CapabilityGeneralization", "MissionInvolvement", "SequenceLink", - "PortAllocation", - "InterfaceItemAllocation", - "DataAssociation" + "StateTransition", + "Availability", + "ExchangeAllocation", + "InterfaceProvision", + "InterfaceRequirement", + "StateAction" ] }, "Position": { @@ -3118,6 +3120,34 @@ "Event" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "ExchangeCategory": { + "type": "object", + "properties": { + "family": { + "$ref": "#/components/schemas/ExchangeFamily" + }, + "exchanges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Id" + } + } + }, + "required": [ + "family", + "exchanges" + ], + "additionalProperties": false + } + }, + "required": [ + "ExchangeCategory" + ], + "additionalProperties": false } ] } @@ -4089,6 +4119,42 @@ "DataAssociation" ], "additionalProperties": false + }, + { + "type": "object", + "properties": { + "CommunicationMean": { + "type": "object", + "properties": { + "source": { + "$ref": "#/components/schemas/Id" + }, + "target": { + "$ref": "#/components/schemas/Id" + }, + "oriented": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Id" + } + } + }, + "required": [ + "source", + "target", + "oriented", + "items" + ], + "additionalProperties": false + } + }, + "required": [ + "CommunicationMean" + ], + "additionalProperties": false } ] }, @@ -4289,6 +4355,10 @@ }, "default": [], "description": "Unique by edge identity within this diagram. Stored order is retained. Each entry counts against the total model record admission limit. Missing routes in earlier schema-3 models default to empty." + }, + "view_options": { + "$ref": "#/components/schemas/ViewOptions", + "default": {} } }, "required": [ @@ -4485,7 +4555,7 @@ "properties": { "schema_version": { "type": "integer", - "const": 5 + "const": 6 }, "id": { "type": "string", @@ -6139,6 +6209,15 @@ "type": "null" } ] + }, + "communication_endpoints": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommunicationEndpointDecision" + }, + "maxItems": 200, + "default": [], + "description": "Explicit SA component-port pair per selected OA CommunicationMean. Reuse or Create only. Owners follow reviewed participant mappings; no participant-to-port substitution or automatic interface allocation." } }, "required": [ @@ -6652,12 +6731,13 @@ 1, 2, 3, - 4 + 4, + 5 ] }, "to_schema": { "type": "integer", - "const": 5 + "const": 6 }, "preserved_ports": { "type": "array", @@ -6844,11 +6924,11 @@ "required_resolutions" ], "additionalProperties": false, - "description": "Transient observations from explicit V1/V2/V3/V4 to V5 migration. New report lists default to empty when reading older responses. Reading an old envelope neither saves nor changes its revision. Allocation UUIDv5 uses project namespace and v2-interface-item::." + "description": "Transient observations from explicit V1/V2/V3/V4/V5 to V6 migration. New report lists default to empty when reading older responses. Reading an old envelope neither saves nor changes its revision. Allocation UUIDv5 uses project namespace and v2-interface-item::." }, "SourceBundle": { "type": "object", - "description": "Complete source5 workspace (source3/4 migrate explicitly). The entry is a root filename. Files use canonical portable slash-separated relative .arcadia paths and all must be reachable. Includes resolve only against this map, never the service filesystem. Up to 256 files, 32 include edges along a path and 1,024 include edges overall. Serialized bundle JSON is at most 8 MiB, checked again after missing UUIDs are inserted. GET returns exact accepted stable text.", + "description": "Complete source6 workspace (source3/4/5 migrate explicitly). The entry is a root filename. Files use canonical portable slash-separated relative .arcadia paths and all must be reachable. Includes resolve only against this map, never the service filesystem. Up to 256 files, 32 include edges along a path and 1,024 include edges overall. Serialized bundle JSON is at most 8 MiB, checked again after missing UUIDs are inserted. GET returns exact accepted stable text.", "properties": { "entry": { "type": "string" @@ -7126,6 +7206,7 @@ "FunctionOutputItems", "SignalItems", "ExchangeItems", + "CategoryExchanges", "TransitionTriggers", "TransitionEffects", "Constrained" @@ -10792,6 +10873,18 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "Category": { + "$ref": "#/components/schemas/CategoryFacet" + } + }, + "required": [ + "Category" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -11087,6 +11180,18 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "Category": { + "$ref": "#/components/schemas/CategoryReview" + } + }, + "required": [ + "Category" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -11987,6 +12092,327 @@ "items" ], "additionalProperties": false + }, + "ExchangeFamily": { + "type": "string", + "enum": [ + "Work", + "Participant" + ] + }, + "Coverage": { + "type": "string", + "enum": [ + "All", + "InBehavior", + "OutsideBehavior" + ] + }, + "ExchangeLabels": { + "type": "string", + "enum": [ + "Name", + "Items", + "NameAndItems" + ] + }, + "CategoryFacet": { + "type": "string", + "enum": [ + "Family" + ] + }, + "ReviewedExchangeFamily": { + "type": "object", + "properties": { + "oa": { + "oneOf": [ + { + "type": "string", + "enum": [ + "NotApplicable" + ] + }, + { + "type": "object", + "properties": { + "Value": { + "$ref": "#/components/schemas/ExchangeFamily" + } + }, + "required": [ + "Value" + ], + "additionalProperties": false + } + ] + }, + "sa": { + "oneOf": [ + { + "type": "string", + "enum": [ + "NotApplicable" + ] + }, + { + "type": "object", + "properties": { + "Value": { + "$ref": "#/components/schemas/ExchangeFamily" + } + }, + "required": [ + "Value" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "oa", + "sa" + ], + "additionalProperties": false + }, + "CategoryReview": { + "oneOf": [ + { + "type": "object", + "properties": { + "Family": { + "$ref": "#/components/schemas/ReviewedExchangeFamily" + } + }, + "required": [ + "Family" + ], + "additionalProperties": false + } + ] + }, + "EdgeKind": { + "oneOf": [ + { + "type": "string", + "enum": [ + "Trace" + ] + }, + { + "type": "object", + "properties": { + "Relation": { + "$ref": "#/components/schemas/RelationKind" + } + }, + "required": [ + "Relation" + ], + "additionalProperties": false + } + ] + }, + "RelationKinds": { + "oneOf": [ + { + "type": "string", + "enum": [ + "All" + ] + }, + { + "type": "object", + "properties": { + "Only": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EdgeKind" + } + } + }, + "required": [ + "Only" + ], + "additionalProperties": false + } + ] + }, + "CategorySelection": { + "oneOf": [ + { + "type": "string", + "enum": [ + "All" + ] + }, + { + "type": "object", + "properties": { + "Any": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Id" + } + } + }, + "required": [ + "Any" + ], + "additionalProperties": false + } + ] + }, + "ContextSelection": { + "oneOf": [ + { + "type": "string", + "enum": [ + "All" + ] + }, + { + "type": "object", + "properties": { + "Any": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Id" + } + } + }, + "required": [ + "Any" + ], + "additionalProperties": false + } + ] + }, + "ViewOptions": { + "type": "object", + "properties": { + "hidden_edges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Id" + }, + "default": [] + }, + "relation_kinds": { + "$ref": "#/components/schemas/RelationKinds", + "default": "All" + }, + "work_categories": { + "$ref": "#/components/schemas/CategorySelection", + "default": "All" + }, + "participant_categories": { + "$ref": "#/components/schemas/CategorySelection", + "default": "All" + }, + "coverage": { + "$ref": "#/components/schemas/Coverage", + "default": "All" + }, + "work_contexts": { + "$ref": "#/components/schemas/ContextSelection", + "default": "All" + }, + "exchange_labels": { + "$ref": "#/components/schemas/ExchangeLabels", + "default": "Name" + } + }, + "required": [], + "additionalProperties": false, + "description": "Presentation only. Independent predicates intersect; category members union within a family. Only([]) hides all relations and traces. Any([]) remains an explicit empty selection. Traceability permits both-layer hidden/category/context references; ordinary projections require the diagram layer. Filtering preserves unfiltered automatic port positions." + }, + "CommunicationPortChoice": { + "oneOf": [ + { + "type": "object", + "properties": { + "Reuse": { + "type": "object", + "properties": { + "port": { + "$ref": "#/components/schemas/Id" + } + }, + "required": [ + "port" + ], + "additionalProperties": false + } + }, + "required": [ + "Reuse" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "Create": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/Id" + }, + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kind": { + "$ref": "#/components/schemas/ComponentPortKind" + }, + "orientation": { + "$ref": "#/components/schemas/ComponentPortOrientation" + } + }, + "required": [ + "id", + "alias", + "name", + "description", + "kind", + "orientation" + ], + "additionalProperties": false + } + }, + "required": [ + "Create" + ], + "additionalProperties": false + } + ] + }, + "CommunicationEndpointDecision": { + "type": "object", + "properties": { + "source": { + "$ref": "#/components/schemas/Id" + }, + "source_port": { + "$ref": "#/components/schemas/CommunicationPortChoice" + }, + "target_port": { + "$ref": "#/components/schemas/CommunicationPortChoice" + } + }, + "required": [ + "source", + "source_port", + "target_port" + ], + "additionalProperties": false } } } diff --git a/docs/scope.md b/docs/scope.md index 796ab8cf..7dc13be6 100644 --- a/docs/scope.md +++ b/docs/scope.md @@ -6,14 +6,14 @@ Arcadia Studio implements a systems-engineering workbench spanning Operational A | Area | Implemented semantics and authoring | |---|---| -| Operational Analysis | Entities, actors, roles, activities, missions, capabilities and processes; entity/work decomposition; leaf allocation; role realization/assignment; interactions and capability/process involvement | +| Operational Analysis | Entities, actors, roles, activities, missions, capabilities and processes; entity/work decomposition; leaf allocation; role realization/assignment; interactions, direct participant Communication means with direction/items and Interaction allocations, and capability/process involvement | | System Analysis | One System of Interest, external Actors, functions, missions, capabilities and functional chains; functional decomposition independent of the system boundary; allocation, functional/boundary exchanges and mission-capability links | | Data and interfaces | Information/material/energy nature and Flow/Event/Operation/SharedData mechanisms, bounded primitive/enum/record types, typed defaults, field multiplicity/units/direction/storage, specialization, identified interface allocations and reference associations, separate function input/output and Standard/Flow component ports, interface provision/requirement, port and exchange allocations | | Behavior | Process/chain work and exchange contributions, control topology and sequence reuse; scenario lifelines, correlated calls/replies, timers, action/state occurrences, ALT/PAR/LOOP/OPTIONAL fragments, scenario reuse and pre/postconditions | | States and conditions | Participant-owned state machines, nested regions, states/modes and typed pseudostates, signal/time/change events, trigger/guard/effect transitions, linked entry/do/exit work, availability and constraints | -| Perspectives and evidence | Bidirectional counterpart creation/linking, reviewed descriptive and typed semantic changes, many-to-many typed/manual traces, arbitrary artifact URI/revision/fragment references | -| Views and editing | Independent diagrams, decomposition/allocation nesting, collapse, pan/zoom/Fit/Arrange, editable connection routes and labels, four-side port positioning, direct interaction/functional/boundary exchange tools, contextual menus, SVG export, model explorer and typed properties | -| Text and storage | `arcadia 5` source projects with relative includes and nested declarations, stable aliases/UUIDs, a source-file tree, per-file drafts and diagnostics, comment-preserving graphical updates, exact model/source Undo/Redo, legacy migration, atomic saves, interrupted source-save recovery, Home/recents and private unsaved-work checkpoints | +| Perspectives and evidence | Bidirectional counterpart creation/linking, reviewed descriptive and typed semantic changes, explicit ordered collections and combined multi-source review, many-to-many typed/manual traces, arbitrary artifact URI/revision/fragment references | +| Views and editing | Independent and duplicated diagrams, decomposition/allocation nesting, collapse, pan/zoom/Fit/Arrange, editable connection routes and labels, four-side port positioning, direct interaction/communication/functional/boundary exchange tools, named exchange categories, saved line-kind/category/coverage/context/hide/label settings, contextual menus, SVG export, model explorer and typed properties | +| Text and storage | `arcadia 6` source projects with relative includes and nested declarations, stable aliases/UUIDs, a source-file tree, per-file drafts and diagnostics, comment-preserving graphical updates, exact model/source Undo/Redo, legacy migration, per-file atomic replacement with interrupted source-save recovery, Home/recents and private unsaved-work checkpoints | | Automation and hosted editing | Rust REST service for project/source-bundle/query/validation/transaction/transition/artifact workflows; optional desktop catalog/open/publish, explicit conditional Save, server review and local Save as; ETag preconditions, bounded requests and atomic current-revision storage | Graphical and textual parity applies to the persistent fields of this profile. Diagram placements are model data; camera, selection, source cursor and undo history are session state. Canvas gestures need not match textual operations one for one: typed inspectors and explicit commands expose precise semantics. Full source replacement must repair dependencies in its candidate, while graphical delete/reidentify commands maintain references automatically. @@ -23,9 +23,10 @@ Graphical and textual parity applies to the persistent fields of this profile. D - OA captures operational needs; SA defines the selected system contribution. SA contains no internal logical/physical components, and Studio does not decompose SA Actors. The System may be absent while authoring, with a completeness warning. - Work decomposition and performer allocation are independent. Only leaves have direct allocations. A parent drawn within one performer can be a computed leaf summary, not an additional allocation. Role sharing does not create several direct performers for one function. - Nesting expresses semantic containment or the selected allocation projection. Placement movement does not change ownership or allocation. Hide and delete have different effects. -- Counterpart creation copies mapped typed content and required references. Existing-counterpart semantic review compares typed facts and prior observations, with explicit keep, take or final-value decisions for supported scalar, structural, constructor and individual collection-row changes. Reference mapping and candidate validation apply before an undoable transaction. Ordered collections, attachment/artifact and multi-source GUI reconciliation remain unfinished. Review does not automatically synchronize arbitrary structure or overwrite the other perspective. Creation scope follows ownership/reference dependencies, not every incident relationship. +- Counterpart creation copies mapped typed content and required references. Existing-counterpart semantic review compares typed facts and prior observations, with explicit keep, take or final-value decisions for supported scalar, structural, constructor and individual collection-row changes. Reference mapping and candidate validation apply before an undoable transaction. Ordered collections have explicit final-order review with retained rejected drafts. Included trace rows can combine multiple source contributions into one validated model/history transaction; conflicting decisions and incompatible reference mappings remain explicit blockers. Existing Child/Incident attachment fields and artifact lifecycle review are implemented. Artifact creation reserves an explicit counterpart identity and requires all seven metadata decisions; identified source-owner absence can delete a link, while destination-only Take retains it. Owner moves and kept Presence must agree. Latest attachment/artifact native acceptance remains pending. Review does not automatically synchronize arbitrary structure or overwrite the other perspective. Creation scope follows ownership/reference dependencies, not every incident relationship. - Processes/chains use contributions and explicit sequence links; imported ordered exchange paths retain their implied order until topology conversion. Rich fragments belong to scenarios. Guards, expressions and state behavior text are descriptive; there is no execution engine. Mixed states and modes are permitted by this Studio profile. -- Preserved OA boundary exchanges use Legacy ports. Capability grouping is containment; include, extend and generalization use separate typed relationships in OA and SA. Data/item references remain within their layer and can have explicit transitioned counterparts. +- OA Communication means connect Entities/Actors directly and can allocate Interactions whose actual performers match their endpoints and direction. Transition to SA requires explicit compatible boundary-port decisions; the tool does not guess system ports. Preserved OA boundary exchanges use Legacy ports. Capability grouping is containment; include, extend and generalization use separate typed relationships in OA and SA. Data/item references remain within their layer and can have explicit transitioned counterparts. +- Saved view options preserve semantic records, placements and routes. Work and Participant categories are independent families; selected categories combine by union and explicit empty selections match none. Global behavior coverage and selected process/chain/scenario evidence intersect with category choices. Manual hides remain independent of labels and other filters. Traceability permits both layers' eligible references; ordinary views retain their declared-layer scope. Duplicating a view gives its diagram and placements fresh identities while preserving semantic references and presentation settings. - Applying a source project preserves authored comments and formatting while assigning missing identities. Graphical updates preserve unaffected source bytes; canonical export is a separate operation. JSON does not preserve comments or source-file organization. See the [source contract](language.rst) for comment ownership and update rules. - Port position and connection routing belong to each diagram and do not change semantic ownership or endpoints. Folding may temporarily promote an edge to a visible container; expansion restores its authored route. [Routing verification](../harness/release-routing-verification.md) records accepted behavior and measured limits. - Artifact references retain author-provided URI/revision/fragment values without fetching or executing resources. @@ -36,8 +37,8 @@ These are concrete design responses, not claims that every Capella user experien | Concern | Studio response | Remaining tradeoff | |---|---|---| -| Dense workbench and difficult onboarding | Task workspaces, OA/SA context, contextual menus, direct architecture and behavior gestures, searchable guidance, Home/recents and relation/transition builders | Current Home/recovery and expanded review controls still need native acceptance | -| Noisy architecture pictures | Nested ownership/allocation views, independent placements, collapse, automatic routing, editable bends/labels and port positions | Dense diagrams may need manual adjustment; routing has a bounded solve budget | +| Dense workbench and difficult onboarding | Task workspaces, OA/SA context, contextual menus, direct architecture and behavior gestures, searchable guidance, Home/recents and relation/transition builders | Home/recovery and keyed/main ordered review have current Windows evidence; remaining variants and Linux acceptance are tracked separately | +| Noisy architecture pictures | Nested ownership/allocation views, independent and duplicated placements, saved category/coverage/context filters, explicit hides, collapse, automatic routing, editable bends/labels and port positions | Scoped forty-exchange native filtering/duplication/hide/reopening passed; other view combinations remain pending and routing has a bounded solve budget | | Hard-to-review model files | Modular typed source, stable aliases/UUIDs, preserved comments/formatting and source-aware history | Semantic merges, completion and an LSP are absent | | Hidden mutation effects | Validated atomic commands, dependency cleanup, identity maintenance, previewed counterpart changes and undo | No distributed editing or automatic structural reconciliation | | Platform/plugin overhead | Native Rust/Iced desktop and independent headless service | WGPU has substantial dependencies and needs a working graphics adapter | @@ -47,11 +48,15 @@ These are concrete design responses, not claims that every Capella user experien Direct process/scenario/state authoring, typed semantic drift/reconciliation, Home/recents and unsaved-work checkpoints are implemented. Their evidence and outstanding acceptance cases are separated in the [current coverage audit](../harness/release-current-coverage-20260906.md). Direct state editing has earlier scoped Windows/Linux acceptance recorded in [state evidence](../harness/release-state-editing.md); that does not accept every current change. -Required unfinished implementation includes ordinary OA CommunicationMean authoring with an accurate participant-level contract; persisted selective relation/category/coverage views; completeness checks for exchanges on non-leaf work, behavior-context coverage and missions without capabilities; and ordered-collection, attachment, artifact and multi-source semantic-review GUI controls. Existing Legacy OA boundary exchanges and their Interaction allocations remain supported, but do not close the CommunicationMean requirement. +Source-6 CommunicationMean authoring/transition contracts, persisted selective relation/category/coverage views, view duplication and combined multi-source semantic review are implemented and have automated coverage. The source-6 batch recovery and exact Apply/Save/Undo/Redo path, plus scoped forty-exchange filtering/duplication/hide/reopening, now have Windows native evidence. Child/Incident/Artifact semantic-review GUI controls are implemented and have automated coverage; their native acceptance, communication authoring and remaining selective-view cases remain pending. Located completeness warnings for non-leaf exchanges, behavior-context coverage and missing mission/capability context are implemented and verified across core and REST. Explicit ordered review's main native recovery/Apply/Undo/Redo path and reverse Apply/Undo/Redo have exact source comparisons; the reverse trial also exposed a transient save failure on the older binary, and remaining variants are tracked separately. + +An earlier 2026-09-06 automated checkpoint accounted for 943 passing tests and one existing ignored benchmark across the full workspace run and complete affected-suite reruns following three compatibility-expectation corrections. This includes the 48-test HTTP suite, including source-6 acceptance/restart and endpoint-capacity rejection cases. OpenAPI validation passes 39 fixtures and checks 197 component schemas. Exact logs and later changes are identified in [current state](../harness/STATE.md); these totals do not claim a single uninterrupted green workspace run or native acceptance. The later full desktop suite passed 518/518, including artifact lifecycle and diagram-inspector tests; the corrected six-artifact rerun and strict all-target workspace Clippy also passed. These are separate later evidence points, not a recomputed whole-workspace total. The desktop attempts a private checkpoint every 10 seconds while work is unsaved. It retains accepted source, raw source buffers, pending property/behavior forms and supported workflow drafts, including semantic-review input. Home recovery opens a detached unsaved copy requiring Save as; it restores no undo stack and writes neither the original model nor a hosted destination. Interrupted source-save recovery is a separate mechanism for an incomplete save transaction. -Remaining release gates include current-tree automated checks, expanded data/semantic-review and Home/recovery native acceptance, outstanding behavior and hosted workflow cases, default/minimum-window Windows/Linux acceptance, fresh build/MSRV and specification/OpenAPI checks, real REST/restart/parity verification, packages/checksums and independent final review. Guidance must follow the accepted controls. These are unfinished release requirements, not exclusions or release-acceptance claims. +The [current Windows native record](../harness/release-current-native-verification.md) covers Home/recents, raw invalid source and UUID-property crash recovery, pending timed-transition recovery, keyed semantic recovery and retained rejected ordered input. It records exact repair/Save/Undo/Redo source comparisons and default/minimum-window observations for those workflows. The [timed-bundle oracle](../harness/release-timed-bundle-oracle-20260906.md) independently reopens both saved files and checks the complete intended graph delta. These scoped results do not accept every recovery adapter, current source-6 native workflows, final packages or Linux. + +Remaining release gates include final-tree strict checks and rebuilt binaries, source-6 communication, remaining selective-view cases and latest attachment/artifact native acceptance, expanded data/semantic-review and remaining recovery variants, outstanding behavior and hosted workflow cases, default/minimum-window Windows/Linux acceptance, fresh MSRV and final specification/OpenAPI checks, remaining real REST/restart/parity scenarios, packages/checksums and independent final review. The Windows journal-publication fix has [bounded independent review and automated evidence](../harness/release-journal-publication-review-20260906.md); its new-binary native Save/reopen check remains pending. Guidance must follow the accepted controls. These are unfinished release requirements, not exclusions or release-acceptance claims. ## Remaining boundaries diff --git a/examples/release1/README.md b/examples/release1/README.md new file mode 100644 index 00000000..d877c65b --- /dev/null +++ b/examples/release1/README.md @@ -0,0 +1,86 @@ +# Restore safe water pressure + +This source-6 project follows one engineering question: after a pressure alarm, can the control room restore service remotely, or must a technician repair a valve? It connects the operational need to an explicitly chosen coordination-service boundary. Allow about 25 minutes for the walkthrough. + +Copy the whole `examples/release1` folder to your working location, then open the copied `project.arcadia` in Arcadia Studio. Keep the folder together: the entry includes 29 modules, and the evidence link is relative to it. If you instead use Save As to a new folder, it copies model source files only: also copy `evidence/` beside the new entry and keep this README available. Save As does not copy external artifact targets. Existing older examples remain useful migration exercises. + +The numbers, guards and states describe an illustrative design. The model does not simulate hydraulics, execute a control algorithm, or establish the safety of an actual water network. + +## 1. Start with responsibility, not equipment + +Open saved view **1 · OA responsibility and repair traffic**. The **Control room** assesses the alarm, resets the valve and confirms pressure. The **Field technician** performs field repair. The parent activity **Restore safe pressure** groups these leaf activities; responsibility is allocated to the leaves. + +Inspect **Technician service conversation** (`oa_contact`). This unoriented CommunicationMean connects operational participants. **Request field repair** and **Report repair result** are Interactions between their allocated activities. Each Interaction is explicitly allocated to the communication mean. No OA port or implementation component has been introduced. + +Inspect the **Valve repair** exchange item and its `pressure` parameter. The parameter has direction InOut, Value storage, multiplicity 1..1, and a real-valued type bounded from 0 to 600 with unit text `kPa`. The unit documents the contract; no conversion is inferred. + +## 2. Follow the alternative and its reusable procedure + +Open process **Handle a pressure alarm** (`oa_response`) in the behavior workspace. Its first step references **Assess an alarm procedure** (`oa_assessment_procedure`), whose Action uses the assessment activity and whose ChainInvolvement supports **Assess an alarm**. Follow that reference to the Choice in the main process. The guarded branch allows an authorized remote reset; the `else` branch references **Field repair procedure** (`oa_field_procedure`). Both paths merge before confirmation. The explicit SequenceLinks define flow; the list of steps alone does not imply those branches. + +Open the referenced procedure. It contains request exchange, repair action, then report exchange. The parent process reuses that procedure by identity. It does not copy its actions. The main process contributes to the restoration capability. + +Open scenario **Repair request and measured reply** (`oa_conversation`). The Control room calls the technician using the communication mean, supplying `420.0`. The technician's correlated Reply returns `320.0` on the reversed participant pair. The reply points to the actual Call. The enclosing scenario (`oa_full_scenario`) reuses this conversation with two explicit lifeline bindings. + +The mission **Maintain safe water supply** links to the restoration capability, which includes the assessment capability. The repair conversation has the restoration context: it begins after assessment and coordinates repair. The assessment procedure supplies the separate assessment behavior support. + +## 3. Examine the explicit SA engineering decisions + +Open **2 · SA service boundary and functions**. The chosen System is **Pressure coordination service**; the technician remains outside as a SystemActor. OA activities have traced SA functions. The communication mean has a traced SA BoundaryExchange (`sa_contact`), with two explicitly chosen Standard component ports. The two Interactions have traced FunctionalExchanges with directional function ports. + +Inspect **Valve repair service** (`sa_repair_interface`), its synchronous Operation acceptance, and the client Requires / provider Provides links. Four PortAllocations connect the function ports to their component ports; two ExchangeAllocations connect functional traffic to the boundary exchange. These are explicit contract refinements. An operational channel cannot choose the technical interface or ports on the engineer's behalf. + +The generator performs the real OA→SA transition. `transition-before.json` is the OA model plus the two intentionally named SA participants. `transition-request.json` contains their explicit mappings and both component-port Create decisions. It deliberately leaves the operational ExchangeAllocations out of this first transition: those allocations require the later reviewed SA interface and port refinements. Omitting these allocations is explicit, not silently guessed. + +For a fresh transition exercise, import `transition-before.json`, select the OA content, map Control room to the existing System and Field technician to the existing SystemActor, and review both communication endpoint ports. The supplied request is the exact programmatic counterpart. The generated final project then adds the interface/port allocations, views, evidence and one unfinished function. Fresh transition UUIDs are normalized by the generator for stable published files, so an interactive replay is semantically comparable rather than byte-identical to `model.json`. + +## 4. Compare views without changing the architecture + +Switch between **2 · SA service boundary and functions** and **3 · SA service conversation only**. They place the same semantic identities. The focused view uses the participant exchange category, an explicitly empty work-category selection, and NameAndItems labels. It shows the boundary conversation and hides functional traffic. The underlying exchanges and their port positions remain unchanged. + +Open View properties to inspect the selections. Clearing a filter does not delete a relation. The category is an engineering collection of existing exchanges; it is not a replacement exchange. **4 · SA mission and included capability** supplies a separate goal view. + +Open **5 · SA pressure supervision** in the state workspace. Follow the initial state to monitoring, the typed high-pressure Change event into waiting, and the guarded return. Waiting has an Entry action linked to the assessment function. This is an event/action relationship, not free prose embedded in a transition label. + +## 5. Investigate the deliberate warning + +Model checks should report exactly one warning: `work_without_behavior_context` on **Audit pressure thresholds** (`sa_audit_thresholds`). It has a responsible System but no process/chain or scenario use. `expected-diagnostics.json` records the expected located issue. + +Decide whether threshold auditing belongs in the response process, in a separate maintenance behavior, or outside the intended model. Add the appropriate behavior use or remove the explicitly out-of-scope function. Merely hiding it in a view would not resolve the coverage question. The supplied model intentionally leaves this decision open. + +## 6. Make one reviewed change and inspect its source + +1. In OA, rename **Confirm safe pressure** (`oa_confirm`) to **Confirm stable pressure** and Apply the edit. +2. Open semantic OA→SA review for that activity and its existing function counterpart (`sa_confirm`). Review the Header Name facet and choose Take source. Preview, then Apply the reviewed change. +3. Confirm that the SA function name changed while the deliberately different System name remains **Pressure coordination service**. +4. In Source, inspect `sa/functions/function_sa_restore_work.arcadia` and `traceability.arcadia`. The counterpart name and review history change. Unrelated modules retain their text. Use Undo/Redo in this editing session to inspect the reviewed transaction; then Save and reopen to check persistence. Undo history is session-local; reopening restores the saved model, not the previous undo stack. + +The integration oracle executes this exact semantic review and source update. It separately proves that changing the request Interaction to target a Control-room-owned activity violates the communication allocation's performer pairing: command and source-update paths reject the invalid candidate together. + +## Source, service and reproducibility + +| File or folder | Purpose | +|---|---| +| `project.arcadia` | Openable source-6 entry with all module includes | +| `oa/`, `sa/` | Participants, work, data, capabilities and reusable behavior | +| `views/` | Five saved views; filters and placements are presentation state | +| `traceability.arcadia` | OA↔SA correspondence and review history | +| `model.json` | Current schema-6 interchange of the final accepted model | +| `transition-before.json`, `transition-request.json` | Explicit first-transition replay inputs | +| `evidence/repair-check.md` | Local teaching evidence linked from the SA conversation | +| `model.rs` | Deterministic lesson builder shared by generator and tests | + +The REST routes are documented in [the API reference](../../docs/api.rst). Import `model.json` with `POST /api/v1/projects/import`; query the returned resource's `/model` and `/validation`. Expect schema 6 and the one warning above. JSON import provides canonical source; to retain the exact modular source layout, use `/api/v1/projects/import-source-project` with a SourceBundle containing entry `project.arcadia` and only the 30 reachable `.arcadia` file strings, keyed by slash-separated relative paths. Neither route reads the artifact target from your local filesystem. + +For transition replay, import `transition-before.json` instead. Submit `transition-request.json` to that baseline resource's `/transitions/preview` with its current ETag in `If-Match`. Review the returned plan, then follow the documented `/transitions/apply` protocol. Use the baseline resource for this exercise: the final model already contains the mapped records and subsequent refinements. This tutorial's automated evidence is core/source validation; it does not claim a separate native or HTTP acceptance run of this particular example. + +From the repository root, with the supported Rust toolchain available: + +```text +cargo run --locked -p oa-core --example generate_release1 -- examples/release1 +cargo test --locked -p oa-core --test release1_teaching +``` + +Regeneration deliberately overwrites the generated source and JSON in the selected output folder. Copy the lesson before experimenting. No old examples are rewritten. The tests check deterministic regeneration, exact source/interchange parity, explicit transport and reply direction, behavior reuse, view identity/geometry, atomic rejection, and a reviewed name change preserving unrelated modules. + +The available SVG export is for architecture views. Faithful process, scenario and state-scene SVG export is outside this first-release export scope; the interactive behavior workspace is the authoritative rendering for this exercise. diff --git a/examples/release1/evidence/repair-check.md b/examples/release1/evidence/repair-check.md new file mode 100644 index 00000000..b3e9ddf8 --- /dev/null +++ b/examples/release1/evidence/repair-check.md @@ -0,0 +1,15 @@ +# Repair conversation acceptance notes + +Revision: lesson-1. Linked from `sa_conversation` by `repair_test_evidence`, relation Verifies, fragment `acceptance`. + +## Acceptance + +This is a reproducible teaching check, not field-test evidence for a real valve. + +- The Control room calls the Field technician across the operational channel; the correlated Reply reverses the actual Call pair. +- The SA conversation uses the selected service boundary and explicit Standard client/provider ports. A synchronous operation interface and its Requires/Provides links describe the contract. +- Both request and report functional exchanges are allocated through matching function/component ports and the boundary exchange. +- The example carries input/output pressure values `420.0` and `320.0` against the declared 0..600 real type. Values and unit text are descriptive model data; no hydraulic simulation has run. +- The remote reset and field-repair alternatives join before pressure confirmation. The field procedure and scenario conversation are reused through identities and explicit bindings. + +Run `cargo test --locked -p oa-core --test release1_teaching` from the repository root to reproduce the core/source oracles. The intentionally unfinished threshold-audit function remains a located coverage warning. Actual equipment safety assessment, executable control validation and field measurements are separate engineering work. diff --git a/examples/release1/expected-diagnostics.json b/examples/release1/expected-diagnostics.json new file mode 100644 index 00000000..e93a7fa7 --- /dev/null +++ b/examples/release1/expected-diagnostics.json @@ -0,0 +1,9 @@ +[ + { + "code": "work_without_behavior_context", + "element": "4149a872-3440-59de-9879-8981e78d27ab", + "field": null, + "message": "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.", + "severity": "Warning" + } +] diff --git a/examples/release1/model.json b/examples/release1/model.json new file mode 100644 index 00000000..366c2d2d --- /dev/null +++ b/examples/release1/model.json @@ -0,0 +1,4401 @@ +{ + "alias": "pressure_restoration", + "artifact_links": { + "f5922bf7-05d8-590f-86e9-1d09f37f4fa6": { + "alias": "repair_test_evidence", + "description": "A local teaching artifact, not proof of a real valve's safety.", + "fragment": "acceptance", + "id": "f5922bf7-05d8-590f-86e9-1d09f37f4fa6", + "name": "Repair conversation acceptance notes", + "owner": "730b27ca-b039-5a3b-8dc6-03418f9061b3", + "relation": "Verifies", + "revision": "lesson-1", + "uri": "evidence/repair-check.md" + } + }, + "description": "A controller either resets a valve remotely or requests field repair. Compare the operational need with the explicitly chosen coordination-service boundary. Start with README.md beside project.arcadia. Units and guards are descriptive; this model does not simulate a real water network.", + "diagrams": { + "103ea62c-ac56-5827-94e7-cf09bca96ccc": { + "alias": "oa_allocation_view", + "description": "Teaching view: positions and filters do not change ownership or exchange identities.", + "id": "103ea62c-ac56-5827-94e7-cf09bca96ccc", + "layer": "Oa", + "name": "1 · OA responsibility and repair traffic", + "nodes": { + "202ebe37-ddb4-5099-8ce9-ec3e895ab9e9": { + "alias": "oa_allocation_view_oa_repair", + "collapsed": false, + "element": "a21465af-05f3-5fb5-8983-de26932422fb", + "id": "202ebe37-ddb4-5099-8ce9-ec3e895ab9e9", + "position": { + "x": 890.0, + "y": 170.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "3191e05c-1c7f-5f0e-beb0-5f3bded54c48": { + "alias": "oa_allocation_view_oa_control", + "collapsed": false, + "element": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "id": "3191e05c-1c7f-5f0e-beb0-5f3bded54c48", + "position": { + "x": 60.0, + "y": 80.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "5ae1c46d-59a3-50fd-8cc3-b292df21f951": { + "alias": "oa_allocation_view_oa_assess", + "collapsed": false, + "element": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "id": "5ae1c46d-59a3-50fd-8cc3-b292df21f951", + "position": { + "x": 100.0, + "y": 170.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "7066f78c-9736-5349-b94a-0587331b980e": { + "alias": "oa_allocation_view_oa_reset", + "collapsed": false, + "element": "d0c8be87-d238-503a-a549-7558a210507f", + "id": "7066f78c-9736-5349-b94a-0587331b980e", + "position": { + "x": 100.0, + "y": 330.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "d27f28ef-f10a-5355-9a05-6f2fd39b7de6": { + "alias": "oa_allocation_view_oa_technician", + "collapsed": false, + "element": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "id": "d27f28ef-f10a-5355-9a05-6f2fd39b7de6", + "position": { + "x": 850.0, + "y": 80.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "edd74060-ac61-5450-a57c-a6577daa5d50": { + "alias": "oa_allocation_view_oa_confirm", + "collapsed": false, + "element": "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "id": "edd74060-ac61-5450-a57c-a6577daa5d50", + "position": { + "x": 100.0, + "y": 490.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + } + }, + "projection": "Allocation", + "routes": [], + "view_options": { + "coverage": "All", + "exchange_labels": "Name", + "hidden_edges": [], + "participant_categories": "All", + "relation_kinds": "All", + "work_categories": "All", + "work_contexts": "All" + } + }, + "30d5baf9-6581-5cb6-846a-bcaa1e6cce63": { + "alias": "sa_state_view", + "description": "Teaching view: positions and filters do not change ownership or exchange identities.", + "id": "30d5baf9-6581-5cb6-846a-bcaa1e6cce63", + "layer": "Sa", + "name": "5 · SA pressure supervision", + "nodes": { + "55a74f91-2025-5001-8db8-9566b1dbe783": { + "alias": "sa_state_view_sa_monitor_region", + "collapsed": false, + "element": "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c", + "id": "55a74f91-2025-5001-8db8-9566b1dbe783", + "position": { + "x": 70.0, + "y": 95.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "57acbb71-ca86-57fc-ae90-42dad195f0e0": { + "alias": "sa_state_view_sa_waiting", + "collapsed": false, + "element": "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37", + "id": "57acbb71-ca86-57fc-ae90-42dad195f0e0", + "position": { + "x": 650.0, + "y": 200.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "5e2326a0-0f6c-5f8d-8a21-fdded18a9281": { + "alias": "sa_state_view_sa_monitor_initial", + "collapsed": false, + "element": "57eb5348-14d8-560e-996a-8dbf993243e2", + "id": "5e2326a0-0f6c-5f8d-8a21-fdded18a9281", + "position": { + "x": 100.0, + "y": 200.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "a676fbb9-0833-59a8-98cf-a10d8f4b7434": { + "alias": "sa_state_view_sa_monitor", + "collapsed": false, + "element": "132b3f28-3f88-5327-9868-8c8b645ed412", + "id": "a676fbb9-0833-59a8-98cf-a10d8f4b7434", + "position": { + "x": 30.0, + "y": 30.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "b3af7b04-5a45-5b82-ab69-7eada4b876f0": { + "alias": "sa_state_view_sa_monitoring", + "collapsed": false, + "element": "8af21f9b-c21e-5aae-af0d-0958840f6655", + "id": "b3af7b04-5a45-5b82-ab69-7eada4b876f0", + "position": { + "x": 220.0, + "y": 200.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + } + }, + "projection": { + "StateMachine": { + "machine": "132b3f28-3f88-5327-9868-8c8b645ed412" + } + }, + "routes": [], + "view_options": { + "coverage": "All", + "exchange_labels": "Name", + "hidden_edges": [], + "participant_categories": "All", + "relation_kinds": "All", + "work_categories": "All", + "work_contexts": "All" + } + }, + "35d3c24b-289e-59e5-a610-8333e2c6a4bb": { + "alias": "sa_allocation_view", + "description": "Teaching view: positions and filters do not change ownership or exchange identities.", + "id": "35d3c24b-289e-59e5-a610-8333e2c6a4bb", + "layer": "Sa", + "name": "2 · SA service boundary and functions", + "nodes": { + "34630a18-41b0-5fdd-bff4-ee86e93e8651": { + "alias": "sa_allocation_view_sa_repair", + "collapsed": false, + "element": "9497e10f-04c4-5586-8738-2464a6f00bd4", + "id": "34630a18-41b0-5fdd-bff4-ee86e93e8651", + "position": { + "x": 890.0, + "y": 170.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "60f6ad74-b69e-5aef-b832-f1bd60b903ae": { + "alias": "sa_allocation_view_sa_assess", + "collapsed": false, + "element": "dddae020-a444-5d48-acc8-139d2696a672", + "id": "60f6ad74-b69e-5aef-b832-f1bd60b903ae", + "position": { + "x": 100.0, + "y": 170.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "73406244-ebd7-5995-86c7-e17c73c50b32": { + "alias": "sa_allocation_view_sa_reset", + "collapsed": false, + "element": "09342ccb-044b-5483-ac58-87dc3cf7831c", + "id": "73406244-ebd7-5995-86c7-e17c73c50b32", + "position": { + "x": 100.0, + "y": 330.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "7e3ba36f-bc68-5520-bdb9-77083bb7bba2": { + "alias": "sa_allocation_view_sa_confirm", + "collapsed": false, + "element": "d75eb3fa-a27d-52d5-8782-8b342b3865f6", + "id": "7e3ba36f-bc68-5520-bdb9-77083bb7bba2", + "position": { + "x": 100.0, + "y": 490.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "8345c76a-8d85-5c31-902e-ec1cd33ed4a3": { + "alias": "sa_allocation_view_sa_control", + "collapsed": false, + "element": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "id": "8345c76a-8d85-5c31-902e-ec1cd33ed4a3", + "position": { + "x": 60.0, + "y": 80.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "f7225d1c-e506-505d-bb20-916950269d4b": { + "alias": "sa_allocation_view_sa_technician", + "collapsed": false, + "element": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a", + "id": "f7225d1c-e506-505d-bb20-916950269d4b", + "position": { + "x": 850.0, + "y": 80.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + } + }, + "projection": "Allocation", + "routes": [], + "view_options": { + "coverage": "All", + "exchange_labels": "Name", + "hidden_edges": [], + "participant_categories": "All", + "relation_kinds": "All", + "work_categories": "All", + "work_contexts": "All" + } + }, + "3e6ad194-61b5-584c-b0f0-a78a20af5781": { + "alias": "sa_capability_view", + "description": "Teaching view: positions and filters do not change ownership or exchange identities.", + "id": "3e6ad194-61b5-584c-b0f0-a78a20af5781", + "layer": "Sa", + "name": "4 · SA mission and included capability", + "nodes": { + "069a3fb3-077c-59d8-91da-27b0665143c7": { + "alias": "sa_capability_view_sa_triage", + "collapsed": false, + "element": "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f", + "id": "069a3fb3-077c-59d8-91da-27b0665143c7", + "position": { + "x": 800.0, + "y": 260.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "41ddb68b-bd2b-5f91-82eb-efde01157828": { + "alias": "sa_capability_view_sa_control", + "collapsed": false, + "element": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "id": "41ddb68b-bd2b-5f91-82eb-efde01157828", + "position": { + "x": 60.0, + "y": 60.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "4b814335-bf98-5b68-85d5-ea549766517a": { + "alias": "sa_capability_view_sa_mission", + "collapsed": false, + "element": "5ad034b1-084d-5273-9e70-971671766132", + "id": "4b814335-bf98-5b68-85d5-ea549766517a", + "position": { + "x": 420.0, + "y": 60.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "d3bc13f9-7726-5280-83fe-63f391c45afb": { + "alias": "sa_capability_view_sa_restore", + "collapsed": false, + "element": "81259051-370a-512d-a3b5-6420a6f933b6", + "id": "d3bc13f9-7726-5280-83fe-63f391c45afb", + "position": { + "x": 420.0, + "y": 260.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + } + }, + "projection": "Capabilities", + "routes": [], + "view_options": { + "coverage": "All", + "exchange_labels": "Name", + "hidden_edges": [], + "participant_categories": "All", + "relation_kinds": "All", + "work_categories": "All", + "work_contexts": "All" + } + }, + "50af4c9b-93ce-5ff5-a1f8-7a81eb97e779": { + "alias": "sa_service_view", + "description": "Teaching view: positions and filters do not change ownership or exchange identities.", + "id": "50af4c9b-93ce-5ff5-a1f8-7a81eb97e779", + "layer": "Sa", + "name": "3 · SA service conversation only", + "nodes": { + "40210589-69f7-5209-b329-10bb83cf615c": { + "alias": "sa_service_view_sa_technician", + "collapsed": false, + "element": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a", + "id": "40210589-69f7-5209-b329-10bb83cf615c", + "position": { + "x": 850.0, + "y": 80.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "439f1e45-e5ed-567c-978b-4c93ef267db1": { + "alias": "sa_service_view_sa_assess", + "collapsed": false, + "element": "dddae020-a444-5d48-acc8-139d2696a672", + "id": "439f1e45-e5ed-567c-978b-4c93ef267db1", + "position": { + "x": 100.0, + "y": 170.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "6f138672-149b-5f88-ab07-7fc413a42262": { + "alias": "sa_service_view_sa_confirm", + "collapsed": false, + "element": "d75eb3fa-a27d-52d5-8782-8b342b3865f6", + "id": "6f138672-149b-5f88-ab07-7fc413a42262", + "position": { + "x": 100.0, + "y": 490.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "ae36489d-6ec8-5f1c-b603-8b497227efba": { + "alias": "sa_service_view_sa_repair", + "collapsed": false, + "element": "9497e10f-04c4-5586-8738-2464a6f00bd4", + "id": "ae36489d-6ec8-5f1c-b603-8b497227efba", + "position": { + "x": 890.0, + "y": 170.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "d52da4e6-40b7-5dbc-84fb-80c443711c69": { + "alias": "sa_service_view_sa_reset", + "collapsed": false, + "element": "09342ccb-044b-5483-ac58-87dc3cf7831c", + "id": "d52da4e6-40b7-5dbc-84fb-80c443711c69", + "position": { + "x": 100.0, + "y": 330.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + }, + "f53d4352-b042-56a6-8b3e-c51cd27254f9": { + "alias": "sa_service_view_sa_control", + "collapsed": false, + "element": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "id": "f53d4352-b042-56a6-8b3e-c51cd27254f9", + "position": { + "x": 60.0, + "y": 80.0 + }, + "size": { + "height": 110.0, + "width": 245.0 + } + } + }, + "projection": "Allocation", + "routes": [], + "view_options": { + "coverage": "All", + "exchange_labels": "NameAndItems", + "hidden_edges": [], + "participant_categories": { + "Any": [ + "781cbe75-410f-5cc5-aeb8-a43491f77b99" + ] + }, + "relation_kinds": "All", + "work_categories": { + "Any": [] + }, + "work_contexts": "All" + } + } + }, + "elements": { + "01490fa8-2f01-5ac8-9722-d55d45414379": { + "alias": "sa_high_pressure", + "body": { + "Sa": { + "Shared": { + "Event": { + "body": { + "Change": { + "expression": "measured_pressure > operating_limit" + } + } + } + } + } + }, + "description": "", + "id": "01490fa8-2f01-5ac8-9722-d55d45414379", + "name": "Pressure alarm detected", + "parent": null + }, + "022d3cfa-bbaf-5e13-924a-8e55099b8745": { + "alias": "oa_assessment_procedure", + "body": { + "Oa": { + "Process": { + "steps": [ + "942975b8-0bef-5ae8-9fcb-c37497833fb0" + ] + } + } + }, + "description": "", + "id": "022d3cfa-bbaf-5e13-924a-8e55099b8745", + "name": "Assess an alarm procedure", + "parent": null + }, + "09342ccb-044b-5483-ac58-87dc3cf7831c": { + "alias": "sa_reset", + "body": { + "Sa": "Function" + }, + "description": "", + "id": "09342ccb-044b-5483-ac58-87dc3cf7831c", + "name": "Reset valve remotely", + "parent": "58867e7d-916e-5ebc-9e4c-8dbed1314be3" + }, + "0b07b9ed-492d-57f6-b0b1-ce8a709238a7": { + "alias": "oa_assess_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "SequenceReference": { + "sequence": "022d3cfa-bbaf-5e13-924a-8e55099b8745" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "name": "Assess the alarm", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "0f195fea-3493-5ebd-966d-da9eddeb05d7": { + "alias": "sa_repair_step", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "9497e10f-04c4-5586-8738-2464a6f00bd4", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "0f195fea-3493-5ebd-966d-da9eddeb05d7", + "name": "Repair the valve", + "parent": "93eb6479-54e9-59cb-ac7b-16912347bfc0" + }, + "132b3f28-3f88-5327-9868-8c8b645ed412": { + "alias": "sa_monitor", + "body": { + "Sa": { + "Shared": "StateMachine" + } + }, + "description": "", + "id": "132b3f28-3f88-5327-9868-8c8b645ed412", + "name": "Pressure supervision", + "parent": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" + }, + "155e4fe4-9e3b-5f4e-b886-28abcc3f086b": { + "alias": "sa_outer_technician", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a" + } + } + } + }, + "description": "", + "id": "155e4fe4-9e3b-5f4e-b886-28abcc3f086b", + "name": "Technician", + "parent": "f746a1cd-ff9f-55c0-b325-01c03d2f4fae" + }, + "1970608f-85d1-5234-ac86-5e2b0df27dc3": { + "alias": "oa_monitor", + "body": { + "Oa": { + "Shared": "StateMachine" + } + }, + "description": "", + "id": "1970608f-85d1-5234-ac86-5e2b0df27dc3", + "name": "Pressure supervision", + "parent": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" + }, + "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771": { + "alias": "oa_contact_category", + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "exchanges": [ + "27b47887-baf0-518b-bf87-f5acbf7d0b93" + ], + "family": "Participant" + } + } + } + }, + "description": "", + "id": "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771", + "name": "Field contact", + "parent": null + }, + "1b91f8a7-0bdb-514e-b1cd-955658e09927": { + "alias": "oa_restore_work", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "1b91f8a7-0bdb-514e-b1cd-955658e09927", + "name": "Restore safe pressure", + "parent": null + }, + "1c9da843-e2d3-5591-8e3f-db083a42867e": { + "alias": "sa_use_conversation", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "ScenarioUse": { + "bindings": [ + { + "local": "1edfcf9e-9bfa-51f1-80d5-62b8fd8bb023", + "referenced": "98c61788-2fee-51f4-8cf9-de48e8c70e9e" + }, + { + "local": "155e4fe4-9e3b-5f4e-b886-28abcc3f086b", + "referenced": "6249b54a-e396-5338-82b9-89266c55670d" + } + ], + "scenario": "730b27ca-b039-5a3b-8dc6-03418f9061b3" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "1c9da843-e2d3-5591-8e3f-db083a42867e", + "name": "Use the reviewed repair conversation", + "parent": "f746a1cd-ff9f-55c0-b325-01c03d2f4fae" + }, + "1edfcf9e-9bfa-51f1-80d5-62b8fd8bb023": { + "alias": "sa_outer_controller", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" + } + } + } + }, + "description": "", + "id": "1edfcf9e-9bfa-51f1-80d5-62b8fd8bb023", + "name": "Controller", + "parent": "f746a1cd-ff9f-55c0-b325-01c03d2f4fae" + }, + "2011b3ac-614b-5a47-8891-7ff81965f425": { + "alias": "oa_outer_controller", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" + } + } + } + }, + "description": "", + "id": "2011b3ac-614b-5a47-8891-7ff81965f425", + "name": "Controller", + "parent": "d6f881a4-95da-54e9-ad97-1b8965532629" + }, + "20472233-5ce6-5ffe-b4ad-0bdbccf3e423": { + "alias": "oa_conversation", + "body": { + "Oa": { + "Shared": { + "Scenario": { + "context": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "postcondition": "The controller has received the measured pressure after repair", + "precondition": "A pressure alarm has been assessed", + "steps": [ + "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "cc2d497a-fd15-5455-9ee5-e70eb95811d8" + ], + "viewpoint": "ParticipantInteraction" + } + } + } + }, + "description": "", + "id": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423", + "name": "Repair request and measured reply", + "parent": null + }, + "24a6850e-b819-57e0-af88-ca4ab76581b7": { + "alias": "sa_repair_request_out", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionOutput": { + "outgoing_items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ] + } + }, + "legacy": null + } + } + } + }, + "description": "", + "id": "24a6850e-b819-57e0-af88-ca4ab76581b7", + "name": "Request field repair output", + "parent": "dddae020-a444-5d48-acc8-139d2696a672" + }, + "30c8fad9-a49b-5692-9542-3148e76afd7b": { + "alias": "sa_decision", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Control": { + "kind": "Choice" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "30c8fad9-a49b-5692-9542-3148e76afd7b", + "name": "Can a remote reset safely recover pressure?", + "parent": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "32aa9940-df6e-5a12-b9bd-6337e2d2ed22": { + "alias": "oa_control", + "body": { + "Oa": "Entity" + }, + "description": "", + "id": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "name": "Control room", + "parent": null + }, + "391af7c4-b07d-53a7-8ddd-5c81461fc4f8": { + "alias": "sa_repair_report_in", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionInput": { + "incoming_items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ] + } + }, + "legacy": null + } + } + } + }, + "description": "", + "id": "391af7c4-b07d-53a7-8ddd-5c81461fc4f8", + "name": "Report repair result input", + "parent": "d75eb3fa-a27d-52d5-8782-8b342b3865f6" + }, + "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c": { + "alias": "sa_monitor_region", + "body": { + "Sa": { + "Shared": "Region" + } + }, + "description": "", + "id": "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c", + "name": "Supervision cycle", + "parent": "132b3f28-3f88-5327-9868-8c8b645ed412" + }, + "3dfee23a-148b-5cec-ad2d-b774e84ea5bb": { + "alias": "oa_restore", + "body": { + "Oa": "Capability" + }, + "description": "", + "id": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "name": "Restore a safe supply", + "parent": null + }, + "40d94322-baa2-5c81-b261-ff47cdc33782": { + "alias": "sa_request_step", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "93474111-f23e-56c1-b61b-b7c2396b91bd", + "message_kind": "Asynchronous", + "receiver": null, + "reply_to": null, + "sender": null, + "values": [] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "40d94322-baa2-5c81-b261-ff47cdc33782", + "name": "Request the field repair", + "parent": "93eb6479-54e9-59cb-ac7b-16912347bfc0" + }, + "4149a872-3440-59de-9879-8981e78d27ab": { + "alias": "sa_audit_thresholds", + "body": { + "Sa": "Function" + }, + "description": "Deliberately unfinished: assigned to the service, but no behavior context yet. Model checks should expose this one work_without_behavior_context warning.", + "id": "4149a872-3440-59de-9879-8981e78d27ab", + "name": "Audit pressure thresholds", + "parent": null + }, + "41859a65-ded2-547b-ab03-14e2fa740cf9": { + "alias": "oa_caller", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" + } + } + } + }, + "description": "", + "id": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "name": "Controller", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "435bf87c-73fd-5e4d-978a-4c471bc9eaf0": { + "alias": "sa_control", + "body": { + "Sa": "System" + }, + "description": "", + "id": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "name": "Pressure coordination service", + "parent": null + }, + "47bec4f9-d6d0-5ed1-adca-fa0d960dc416": { + "alias": "sa_confirm_step", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "d75eb3fa-a27d-52d5-8782-8b342b3865f6", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "47bec4f9-d6d0-5ed1-adca-fa0d960dc416", + "name": "Confirm safe pressure", + "parent": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "4e1c0a56-ce80-5f13-a93a-6956de54880c": { + "alias": "sa_reply", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "63009b5f-539e-5914-b630-ec6b7fefb4d5", + "message_kind": "Reply", + "receiver": "98c61788-2fee-51f4-8cf9-de48e8c70e9e", + "reply_to": "8bab4783-a098-5984-9e36-493957caf26c", + "sender": "6249b54a-e396-5338-82b9-89266c55670d", + "values": [ + { + "expression": "320.0", + "field": "bb12ba5d-aab5-5a1b-99de-c767f5365b57", + "path": [] + } + ] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "4e1c0a56-ce80-5f13-a93a-6956de54880c", + "name": "Return pressure after repair", + "parent": "730b27ca-b039-5a3b-8dc6-03418f9061b3" + }, + "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07": { + "alias": "sa_merge", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Control": { + "kind": "Merge" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07", + "name": "Recovery path completed", + "parent": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "5139ce73-56fd-5875-ba3a-e569fc890231": { + "alias": "sa_response", + "body": { + "Sa": { + "FunctionalChain": { + "steps": [ + "9024243c-ccc1-5edb-8477-2aa16e5e9a50", + "30c8fad9-a49b-5692-9542-3148e76afd7b", + "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6", + "d5e61c5d-510b-5091-a878-da46f3a9e152", + "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07", + "47bec4f9-d6d0-5ed1-adca-fa0d960dc416" + ] + } + } + }, + "description": "", + "id": "5139ce73-56fd-5875-ba3a-e569fc890231", + "name": "Handle a pressure alarm", + "parent": null + }, + "57eb5348-14d8-560e-996a-8dbf993243e2": { + "alias": "sa_monitor_initial", + "body": { + "Sa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "Initial" + } + } + } + }, + "description": "", + "id": "57eb5348-14d8-560e-996a-8dbf993243e2", + "name": "Start", + "parent": "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c" + }, + "58867e7d-916e-5ebc-9e4c-8dbed1314be3": { + "alias": "sa_restore_work", + "body": { + "Sa": "Function" + }, + "description": "", + "id": "58867e7d-916e-5ebc-9e4c-8dbed1314be3", + "name": "Restore safe pressure", + "parent": null + }, + "58bae2b0-f560-50f2-818d-19091d22d18a": { + "alias": "oa_response", + "body": { + "Oa": { + "Process": { + "steps": [ + "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "e00594a3-6122-5a6b-b3c2-ecb5506c741d" + ] + } + } + }, + "description": "", + "id": "58bae2b0-f560-50f2-818d-19091d22d18a", + "name": "Handle a pressure alarm", + "parent": null + }, + "58c15833-6e75-523a-b57f-a88f19f6d672": { + "alias": "oa_waiting", + "body": { + "Oa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "State" + } + } + } + }, + "description": "", + "id": "58c15833-6e75-523a-b57f-a88f19f6d672", + "name": "Awaiting repair", + "parent": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" + }, + "5ad034b1-084d-5273-9e70-971671766132": { + "alias": "sa_mission", + "body": { + "Sa": "Mission" + }, + "description": "", + "id": "5ad034b1-084d-5273-9e70-971671766132", + "name": "Maintain safe water supply", + "parent": null + }, + "5ba8079c-2dbe-5e98-ae82-2b60c6020a02": { + "alias": "oa_request_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "message_kind": "Asynchronous", + "receiver": null, + "reply_to": null, + "sender": null, + "values": [] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "name": "Request the field repair", + "parent": "5d3af417-244c-54bb-9588-16daedda4e1f" + }, + "5d3af417-244c-54bb-9588-16daedda4e1f": { + "alias": "oa_field_procedure", + "body": { + "Oa": { + "Process": { + "steps": [ + "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "c59432fe-6092-5ee0-908d-9dbf39476471", + "df2fd3f8-6bc2-5380-a13f-36643ed35ac7" + ] + } + } + }, + "description": "", + "id": "5d3af417-244c-54bb-9588-16daedda4e1f", + "name": "Field repair procedure", + "parent": null + }, + "61f7bbfb-74a4-5540-9182-2583dfb27a49": { + "alias": "oa_assess", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "name": "Assess pressure alarm", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "6249b54a-e396-5338-82b9-89266c55670d": { + "alias": "sa_responder", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a" + } + } + } + }, + "description": "", + "id": "6249b54a-e396-5338-82b9-89266c55670d", + "name": "Technician", + "parent": "730b27ca-b039-5a3b-8dc6-03418f9061b3" + }, + "65d6b8d6-271a-5c84-8b42-abbe310c955e": { + "alias": "oa_high_pressure", + "body": { + "Oa": { + "Shared": { + "Event": { + "body": { + "Change": { + "expression": "measured_pressure > operating_limit" + } + } + } + } + } + }, + "description": "", + "id": "65d6b8d6-271a-5c84-8b42-abbe310c955e", + "name": "Pressure alarm detected", + "parent": null + }, + "6a30b763-cc51-546d-907f-5c63657cfd03": { + "alias": "oa_triage", + "body": { + "Oa": "Capability" + }, + "description": "", + "id": "6a30b763-cc51-546d-907f-5c63657cfd03", + "name": "Assess an alarm", + "parent": null + }, + "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d": { + "alias": "oa_merge", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Control": { + "kind": "Merge" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "name": "Recovery path completed", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae": { + "alias": "oa_monitoring", + "body": { + "Oa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "State" + } + } + } + }, + "description": "", + "id": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "name": "Monitoring", + "parent": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" + }, + "727c0b5c-8db6-5eb7-83d6-735519c5949f": { + "alias": "oa_confirm", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "name": "Confirm safe pressure", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "730b27ca-b039-5a3b-8dc6-03418f9061b3": { + "alias": "sa_conversation", + "body": { + "Sa": { + "Shared": { + "Scenario": { + "context": "81259051-370a-512d-a3b5-6420a6f933b6", + "postcondition": "The controller has received the measured pressure after repair", + "precondition": "A pressure alarm has been assessed", + "steps": [ + "8bab4783-a098-5984-9e36-493957caf26c", + "4e1c0a56-ce80-5f13-a93a-6956de54880c" + ], + "viewpoint": "BoundaryInterface" + } + } + } + }, + "description": "", + "id": "730b27ca-b039-5a3b-8dc6-03418f9061b3", + "name": "Repair request and measured reply", + "parent": null + }, + "781cbe75-410f-5cc5-aeb8-a43491f77b99": { + "alias": "sa_contact_category", + "body": { + "Sa": { + "Shared": { + "ExchangeCategory": { + "exchanges": [ + "63009b5f-539e-5914-b630-ec6b7fefb4d5" + ], + "family": "Participant" + } + } + } + }, + "description": "", + "id": "781cbe75-410f-5cc5-aeb8-a43491f77b99", + "name": "Field contact", + "parent": null + }, + "81259051-370a-512d-a3b5-6420a6f933b6": { + "alias": "sa_restore", + "body": { + "Sa": "Capability" + }, + "description": "", + "id": "81259051-370a-512d-a3b5-6420a6f933b6", + "name": "Restore a safe supply", + "parent": null + }, + "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7": { + "alias": "oa_monitor_region", + "body": { + "Oa": { + "Shared": "Region" + } + }, + "description": "", + "id": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7", + "name": "Supervision cycle", + "parent": "1970608f-85d1-5234-ac86-5e2b0df27dc3" + }, + "87d0a95b-af1b-5352-b753-e4c6533671a5": { + "alias": "sa_pressure", + "body": { + "Sa": { + "Shared": { + "DataType": { + "default": null, + "definition": { + "Real": { + "maximum": 600.0, + "minimum": 0.0 + } + }, + "unit": "kPa" + } + } + } + }, + "description": "", + "id": "87d0a95b-af1b-5352-b753-e4c6533671a5", + "name": "Measured pressure", + "parent": null + }, + "8af21f9b-c21e-5aae-af0d-0958840f6655": { + "alias": "sa_monitoring", + "body": { + "Sa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "State" + } + } + } + }, + "description": "", + "id": "8af21f9b-c21e-5aae-af0d-0958840f6655", + "name": "Monitoring", + "parent": "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c" + }, + "8bab4783-a098-5984-9e36-493957caf26c": { + "alias": "sa_call", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "63009b5f-539e-5914-b630-ec6b7fefb4d5", + "message_kind": "Call", + "receiver": "6249b54a-e396-5338-82b9-89266c55670d", + "reply_to": null, + "sender": "98c61788-2fee-51f4-8cf9-de48e8c70e9e", + "values": [ + { + "expression": "420.0", + "field": "bb12ba5d-aab5-5a1b-99de-c767f5365b57", + "path": [] + } + ] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "8bab4783-a098-5984-9e36-493957caf26c", + "name": "Request valve repair", + "parent": "730b27ca-b039-5a3b-8dc6-03418f9061b3" + }, + "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7": { + "alias": "oa_pressure_parameter", + "body": { + "Oa": { + "Shared": { + "DataField": { + "default": null, + "direction": "InOut", + "maximum": 1, + "minimum": 1, + "storage": "Value", + "type_ref": { + "Named": "e7824764-5db9-5384-8198-2fbaea2b10b2" + }, + "unit": "" + } + } + } + }, + "description": "", + "id": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "name": "Pressure before / after repair", + "parent": "d736c7d6-d369-550a-8ba4-dae223e40bb3" + }, + "9024243c-ccc1-5edb-8477-2aa16e5e9a50": { + "alias": "sa_assess_step", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "SequenceReference": { + "sequence": "acd5a018-f559-5298-a40d-1cf6e1353e74" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "9024243c-ccc1-5edb-8477-2aa16e5e9a50", + "name": "Assess the alarm", + "parent": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "9349b353-53f2-56f8-b571-a05b09335cdf": { + "alias": "oa_repair_traffic", + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "exchanges": [ + "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "b436d0b3-c824-5a96-af6b-2c269816d1cc" + ], + "family": "Work" + } + } + } + }, + "description": "", + "id": "9349b353-53f2-56f8-b571-a05b09335cdf", + "name": "Repair work exchanges", + "parent": null + }, + "93eb6479-54e9-59cb-ac7b-16912347bfc0": { + "alias": "sa_field_procedure", + "body": { + "Sa": { + "FunctionalChain": { + "steps": [ + "40d94322-baa2-5c81-b261-ff47cdc33782", + "0f195fea-3493-5ebd-966d-da9eddeb05d7", + "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac" + ] + } + } + }, + "description": "", + "id": "93eb6479-54e9-59cb-ac7b-16912347bfc0", + "name": "Field repair procedure", + "parent": null + }, + "942975b8-0bef-5ae8-9fcb-c37497833fb0": { + "alias": "oa_assessment_action", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "942975b8-0bef-5ae8-9fcb-c37497833fb0", + "name": "Assess pressure alarm", + "parent": "022d3cfa-bbaf-5e13-924a-8e55099b8745" + }, + "9497e10f-04c4-5586-8738-2464a6f00bd4": { + "alias": "sa_repair", + "body": { + "Sa": "Function" + }, + "description": "", + "id": "9497e10f-04c4-5586-8738-2464a6f00bd4", + "name": "Repair valve in the field", + "parent": "58867e7d-916e-5ebc-9e4c-8dbed1314be3" + }, + "98c61788-2fee-51f4-8cf9-de48e8c70e9e": { + "alias": "sa_caller", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" + } + } + } + }, + "description": "", + "id": "98c61788-2fee-51f4-8cf9-de48e8c70e9e", + "name": "Controller", + "parent": "730b27ca-b039-5a3b-8dc6-03418f9061b3" + }, + "98d411e7-7c65-5c3f-b543-8bf0f2522ea2": { + "alias": "sa_control_port", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + }, + "description": "Explicitly chosen Standard component port for the repair service.", + "id": "98d411e7-7c65-5c3f-b543-8bf0f2522ea2", + "name": "Repair service client", + "parent": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" + }, + "994faed7-dbc8-5b96-9d62-7604ac0fbc3a": { + "alias": "sa_repair_report_out", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionOutput": { + "outgoing_items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ] + } + }, + "legacy": null + } + } + } + }, + "description": "", + "id": "994faed7-dbc8-5b96-9d62-7604ac0fbc3a", + "name": "Report repair result output", + "parent": "9497e10f-04c4-5586-8738-2464a6f00bd4" + }, + "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5": { + "alias": "oa_use_conversation", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "ScenarioUse": { + "bindings": [ + { + "local": "2011b3ac-614b-5a47-8891-7ff81965f425", + "referenced": "41859a65-ded2-547b-ab03-14e2fa740cf9" + }, + { + "local": "fffff882-78fd-5415-928d-c1ef7fac6576", + "referenced": "fc81c5f0-0442-5b88-992b-e5b10f351c59" + } + ], + "scenario": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5", + "name": "Use the reviewed repair conversation", + "parent": "d6f881a4-95da-54e9-ad97-1b8965532629" + }, + "a21465af-05f3-5fb5-8983-de26932422fb": { + "alias": "oa_repair", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "a21465af-05f3-5fb5-8983-de26932422fb", + "name": "Repair valve in the field", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "a4b8942e-a975-5f88-ad14-8f38c345baac": { + "alias": "sa_repair_interface", + "body": { + "Sa": { + "Shared": { + "Interface": { + "supertype": null + } + } + } + }, + "description": "", + "id": "a4b8942e-a975-5f88-ad14-8f38c345baac", + "name": "Valve repair service", + "parent": null + }, + "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37": { + "alias": "sa_waiting", + "body": { + "Sa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "State" + } + } + } + }, + "description": "", + "id": "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37", + "name": "Awaiting repair", + "parent": "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c" + }, + "a7ffb869-9dde-5e76-b276-c719c5c143a9": { + "alias": "oa_monitor_initial", + "body": { + "Oa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "Initial" + } + } + } + }, + "description": "", + "id": "a7ffb869-9dde-5e76-b276-c719c5c143a9", + "name": "Start", + "parent": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" + }, + "a962fc5e-3ab0-5296-8b0d-fd84633f9218": { + "alias": "sa_repair_operation", + "body": { + "Sa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + }, + "description": "", + "id": "a962fc5e-3ab0-5296-8b0d-fd84633f9218", + "name": "Valve repair", + "parent": null + }, + "a9a47e48-100c-57a8-95f0-7fe24da94f94": { + "alias": "sa_repair_traffic", + "body": { + "Sa": { + "Shared": { + "ExchangeCategory": { + "exchanges": [ + "93474111-f23e-56c1-b61b-b7c2396b91bd", + "684eadc3-e670-5779-a60f-bbc9a5bfb062" + ], + "family": "Work" + } + } + } + }, + "description": "", + "id": "a9a47e48-100c-57a8-95f0-7fe24da94f94", + "name": "Repair work exchanges", + "parent": null + }, + "acd5a018-f559-5298-a40d-1cf6e1353e74": { + "alias": "sa_assessment_procedure", + "body": { + "Sa": { + "FunctionalChain": { + "steps": [ + "bae5b192-5b8e-51c6-82ae-2331af0d33bf" + ] + } + } + }, + "description": "", + "id": "acd5a018-f559-5298-a40d-1cf6e1353e74", + "name": "Assess an alarm procedure", + "parent": null + }, + "b280c6ca-e5f9-5424-a5c1-5676e01c2371": { + "alias": "oa_decision", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Control": { + "kind": "Choice" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "name": "Can a remote reset safely recover pressure?", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "b97437c6-cd7a-5a05-b537-b30a17dd936a": { + "alias": "oa_call", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "message_kind": "Call", + "receiver": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "reply_to": null, + "sender": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "values": [ + { + "expression": "420.0", + "field": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "path": [] + } + ] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "name": "Request valve repair", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f": { + "alias": "sa_triage", + "body": { + "Sa": "Capability" + }, + "description": "", + "id": "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f", + "name": "Assess an alarm", + "parent": null + }, + "bae5b192-5b8e-51c6-82ae-2331af0d33bf": { + "alias": "sa_assessment_action", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "dddae020-a444-5d48-acc8-139d2696a672", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "bae5b192-5b8e-51c6-82ae-2331af0d33bf", + "name": "Assess pressure alarm", + "parent": "acd5a018-f559-5298-a40d-1cf6e1353e74" + }, + "bb12ba5d-aab5-5a1b-99de-c767f5365b57": { + "alias": "sa_pressure_parameter", + "body": { + "Sa": { + "Shared": { + "DataField": { + "default": null, + "direction": "InOut", + "maximum": 1, + "minimum": 1, + "storage": "Value", + "type_ref": { + "Named": "87d0a95b-af1b-5352-b753-e4c6533671a5" + }, + "unit": "" + } + } + } + }, + "description": "", + "id": "bb12ba5d-aab5-5a1b-99de-c767f5365b57", + "name": "Pressure before / after repair", + "parent": "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + }, + "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6": { + "alias": "sa_reset_step", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "09342ccb-044b-5483-ac58-87dc3cf7831c", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6", + "name": "Try the authorized remote reset", + "parent": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "c05b9422-b9da-5de5-b21d-07ad23bd2b8a": { + "alias": "sa_technician", + "body": { + "Sa": "Actor" + }, + "description": "", + "id": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a", + "name": "Field technician", + "parent": null + }, + "c59432fe-6092-5ee0-908d-9dbf39476471": { + "alias": "oa_repair_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "a21465af-05f3-5fb5-8983-de26932422fb", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "c59432fe-6092-5ee0-908d-9dbf39476471", + "name": "Repair the valve", + "parent": "5d3af417-244c-54bb-9588-16daedda4e1f" + }, + "c9c61f50-06e0-5d17-973d-dcc57cd5cc90": { + "alias": "oa_mission", + "body": { + "Oa": "Mission" + }, + "description": "", + "id": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90", + "name": "Maintain safe water supply", + "parent": null + }, + "cc2d497a-fd15-5455-9ee5-e70eb95811d8": { + "alias": "oa_reply", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "message_kind": "Reply", + "receiver": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "reply_to": "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "sender": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "values": [ + { + "expression": "320.0", + "field": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "path": [] + } + ] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "cc2d497a-fd15-5455-9ee5-e70eb95811d8", + "name": "Return pressure after repair", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "d0c8be87-d238-503a-a549-7558a210507f": { + "alias": "oa_reset", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "d0c8be87-d238-503a-a549-7558a210507f", + "name": "Reset valve remotely", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "d2026fc2-74a8-5e22-9942-ff3eb68a3677": { + "alias": "oa_reset_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "d0c8be87-d238-503a-a549-7558a210507f", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "name": "Try the authorized remote reset", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "d2113fb0-a531-54ef-9484-13951b00ab8f": { + "alias": "sa_repair_request_in", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionInput": { + "incoming_items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ] + } + }, + "legacy": null + } + } + } + }, + "description": "", + "id": "d2113fb0-a531-54ef-9484-13951b00ab8f", + "name": "Request field repair input", + "parent": "9497e10f-04c4-5586-8738-2464a6f00bd4" + }, + "d5e61c5d-510b-5091-a878-da46f3a9e152": { + "alias": "sa_use_field", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "SequenceReference": { + "sequence": "93eb6479-54e9-59cb-ac7b-16912347bfc0" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "d5e61c5d-510b-5091-a878-da46f3a9e152", + "name": "Use the field repair procedure", + "parent": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "d6f881a4-95da-54e9-ad97-1b8965532629": { + "alias": "oa_full_scenario", + "body": { + "Oa": { + "Shared": { + "Scenario": { + "context": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "postcondition": "The controller has received the measured pressure after repair", + "precondition": "A pressure alarm has been assessed", + "steps": [ + "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5" + ], + "viewpoint": "ParticipantInteraction" + } + } + } + }, + "description": "", + "id": "d6f881a4-95da-54e9-ad97-1b8965532629", + "name": "Restore supply through field repair", + "parent": null + }, + "d736c7d6-d369-550a-8ba4-dae223e40bb3": { + "alias": "oa_repair_operation", + "body": { + "Oa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + }, + "description": "", + "id": "d736c7d6-d369-550a-8ba4-dae223e40bb3", + "name": "Valve repair", + "parent": null + }, + "d75eb3fa-a27d-52d5-8782-8b342b3865f6": { + "alias": "sa_confirm", + "body": { + "Sa": "Function" + }, + "description": "", + "id": "d75eb3fa-a27d-52d5-8782-8b342b3865f6", + "name": "Confirm safe pressure", + "parent": "58867e7d-916e-5ebc-9e4c-8dbed1314be3" + }, + "dddae020-a444-5d48-acc8-139d2696a672": { + "alias": "sa_assess", + "body": { + "Sa": "Function" + }, + "description": "", + "id": "dddae020-a444-5d48-acc8-139d2696a672", + "name": "Assess pressure alarm", + "parent": "58867e7d-916e-5ebc-9e4c-8dbed1314be3" + }, + "df2fd3f8-6bc2-5380-a13f-36643ed35ac7": { + "alias": "oa_report_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "b436d0b3-c824-5a96-af6b-2c269816d1cc", + "message_kind": "Asynchronous", + "receiver": null, + "reply_to": null, + "sender": null, + "values": [] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "df2fd3f8-6bc2-5380-a13f-36643ed35ac7", + "name": "Report the measured result", + "parent": "5d3af417-244c-54bb-9588-16daedda4e1f" + }, + "e00594a3-6122-5a6b-b3c2-ecb5506c741d": { + "alias": "oa_confirm_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "e00594a3-6122-5a6b-b3c2-ecb5506c741d", + "name": "Confirm safe pressure", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "e07c3952-4a1a-5cc7-b7d5-03a01101ce49": { + "alias": "oa_technician", + "body": { + "Oa": "Actor" + }, + "description": "", + "id": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "name": "Field technician", + "parent": null + }, + "e6d69cbd-99d7-596b-bbe3-dada6118a600": { + "alias": "sa_technician_port", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + }, + "description": "Explicitly chosen Standard component port for the repair service.", + "id": "e6d69cbd-99d7-596b-bbe3-dada6118a600", + "name": "Repair service provider", + "parent": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a" + }, + "e7824764-5db9-5384-8198-2fbaea2b10b2": { + "alias": "oa_pressure", + "body": { + "Oa": { + "Shared": { + "DataType": { + "default": null, + "definition": { + "Real": { + "maximum": 600.0, + "minimum": 0.0 + } + }, + "unit": "kPa" + } + } + } + }, + "description": "", + "id": "e7824764-5db9-5384-8198-2fbaea2b10b2", + "name": "Measured pressure", + "parent": null + }, + "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac": { + "alias": "sa_report_step", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "684eadc3-e670-5779-a60f-bbc9a5bfb062", + "message_kind": "Asynchronous", + "receiver": null, + "reply_to": null, + "sender": null, + "values": [] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac", + "name": "Report the measured result", + "parent": "93eb6479-54e9-59cb-ac7b-16912347bfc0" + }, + "f746a1cd-ff9f-55c0-b325-01c03d2f4fae": { + "alias": "sa_full_scenario", + "body": { + "Sa": { + "Shared": { + "Scenario": { + "context": "81259051-370a-512d-a3b5-6420a6f933b6", + "postcondition": "The controller has received the measured pressure after repair", + "precondition": "A pressure alarm has been assessed", + "steps": [ + "1c9da843-e2d3-5591-8e3f-db083a42867e" + ], + "viewpoint": "BoundaryInterface" + } + } + } + }, + "description": "", + "id": "f746a1cd-ff9f-55c0-b325-01c03d2f4fae", + "name": "Restore supply through field repair", + "parent": null + }, + "fc6763bf-a534-54bf-afcd-57ab285ff07d": { + "alias": "oa_use_field", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "SequenceReference": { + "sequence": "5d3af417-244c-54bb-9588-16daedda4e1f" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "name": "Use the field repair procedure", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "fc81c5f0-0442-5b88-992b-e5b10f351c59": { + "alias": "oa_responder", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" + } + } + } + }, + "description": "", + "id": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "name": "Technician", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "fffff882-78fd-5415-928d-c1ef7fac6576": { + "alias": "oa_outer_technician", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" + } + } + } + }, + "description": "", + "id": "fffff882-78fd-5415-928d-c1ef7fac6576", + "name": "Technician", + "parent": "d6f881a4-95da-54e9-ad97-1b8965532629" + } + }, + "id": "2786d72f-5763-5f33-a57f-2773a4ca1a6c", + "name": "Restore safe water pressure — release 1 walkthrough", + "relations": { + "094b888c-450c-5c4f-971a-7d75db059574": { + "alias": "oa_request_carriage", + "body": { + "ExchangeAllocation": { + "source": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "target": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d" + } + }, + "description": "", + "id": "094b888c-450c-5c4f-971a-7d75db059574", + "name": "Exchange allocation: Request field repair" + }, + "09eaa992-4cdf-5ea3-b748-f10fd74b8cba": { + "alias": "sa_port_request_in", + "body": { + "PortAllocation": { + "source": "e6d69cbd-99d7-596b-bbe3-dada6118a600", + "target": "d2113fb0-a531-54ef-9484-13951b00ab8f" + } + }, + "description": "", + "id": "09eaa992-4cdf-5ea3-b748-f10fd74b8cba", + "name": "Port allocation: Request field repair input" + }, + "0c4c9e58-ff32-5c00-b82a-d408a4be0183": { + "alias": "oa_mission_participant", + "body": { + "MissionInvolvement": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90" + } + }, + "description": "", + "id": "0c4c9e58-ff32-5c00-b82a-d408a4be0183", + "name": "Mission involvement: Maintain safe water supply" + }, + "0f6ac3fb-4ad6-581e-ad14-336472072d5e": { + "alias": "sa_field_merge", + "body": { + "SequenceLink": { + "guard": "", + "source": "d5e61c5d-510b-5091-a878-da46f3a9e152", + "target": "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07" + } + }, + "description": "", + "id": "0f6ac3fb-4ad6-581e-ad14-336472072d5e", + "name": "Continue" + }, + "109e2d6b-7e86-5934-93a5-fab56eced84e": { + "alias": "oa_assess_then_decide", + "body": { + "SequenceLink": { + "guard": "", + "source": "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "target": "b280c6ca-e5f9-5424-a5c1-5676e01c2371" + } + }, + "description": "", + "id": "109e2d6b-7e86-5934-93a5-fab56eced84e", + "name": "Continue" + }, + "13c3bfa4-9254-5ab2-b96f-f65f402adf81": { + "alias": "sa_alarm_transition", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "", + "source": "8af21f9b-c21e-5aae-af0d-0958840f6655", + "target": "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37", + "transition_kind": "External", + "trigger": "", + "triggers": [ + "01490fa8-2f01-5ac8-9722-d55d45414379" + ] + } + }, + "description": "", + "id": "13c3bfa4-9254-5ab2-b96f-f65f402adf81", + "name": "State transition: Awaiting repair" + }, + "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a": { + "alias": "oa_reset_allocation", + "body": { + "Allocation": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "d0c8be87-d238-503a-a549-7558a210507f" + } + }, + "description": "", + "id": "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a", + "name": "Allocation: Reset valve remotely" + }, + "1b245065-7242-5d81-8472-aaeb932054c9": { + "alias": "sa_audit_allocation", + "body": { + "Allocation": { + "source": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "target": "4149a872-3440-59de-9879-8981e78d27ab" + } + }, + "description": "", + "id": "1b245065-7242-5d81-8472-aaeb932054c9", + "name": "Allocation: Audit pressure thresholds" + }, + "1bbd5957-1173-51d8-97e3-ba4f084227ae": { + "alias": "sa_field_path", + "body": { + "SequenceLink": { + "guard": "else", + "source": "30c8fad9-a49b-5692-9542-3148e76afd7b", + "target": "d5e61c5d-510b-5091-a878-da46f3a9e152" + } + }, + "description": "", + "id": "1bbd5957-1173-51d8-97e3-ba4f084227ae", + "name": "else" + }, + "1c1e5634-ba8c-591f-b143-90a27cdc6cd2": { + "alias": "sa_mission_participant", + "body": { + "MissionInvolvement": { + "source": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "target": "5ad034b1-084d-5273-9e70-971671766132" + } + }, + "description": "", + "id": "1c1e5634-ba8c-591f-b143-90a27cdc6cd2", + "name": "Mission involvement: Maintain safe water supply" + }, + "1e738f27-a3a1-526d-b330-6384088b9689": { + "alias": "sa_confirm_path", + "body": { + "SequenceLink": { + "guard": "", + "source": "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07", + "target": "47bec4f9-d6d0-5ed1-adca-fa0d960dc416" + } + }, + "description": "", + "id": "1e738f27-a3a1-526d-b330-6384088b9689", + "name": "Continue" + }, + "1f8b1898-65f6-5360-99cf-b78a71bd50ec": { + "alias": "oa_mission_delivery", + "body": { + "MissionCapability": { + "source": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90", + "target": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb" + } + }, + "description": "", + "id": "1f8b1898-65f6-5360-99cf-b78a71bd50ec", + "name": "Mission capability: Restore a safe supply" + }, + "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97": { + "alias": "oa_normal_transition", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "repair confirmed and pressure is safe", + "source": "58c15833-6e75-523a-b57f-a88f19f6d672", + "target": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "transition_kind": "External", + "trigger": "", + "triggers": [] + } + }, + "description": "", + "id": "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97", + "name": "State transition: Monitoring" + }, + "229ee94f-d147-50f6-9769-b5bd0c03a084": { + "alias": "oa_confirm_path", + "body": { + "SequenceLink": { + "guard": "", + "source": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "target": "e00594a3-6122-5a6b-b3c2-ecb5506c741d" + } + }, + "description": "", + "id": "229ee94f-d147-50f6-9769-b5bd0c03a084", + "name": "Continue" + }, + "27b47887-baf0-518b-bf87-f5acbf7d0b93": { + "alias": "oa_contact", + "body": { + "CommunicationMean": { + "items": [ + "d736c7d6-d369-550a-8ba4-dae223e40bb3" + ], + "oriented": false, + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" + } + }, + "description": "", + "id": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "name": "Technician service conversation" + }, + "2b00270c-79a5-52a5-af96-33346bea531b": { + "alias": "oa_start_monitoring", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "", + "source": "a7ffb869-9dde-5e76-b276-c719c5c143a9", + "target": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "transition_kind": "External", + "trigger": "", + "triggers": [] + } + }, + "description": "", + "id": "2b00270c-79a5-52a5-af96-33346bea531b", + "name": "State transition: Monitoring" + }, + "3cc9ab52-2b00-57fd-8709-aabc75e5d3e3": { + "alias": "sa_reset_allocation", + "body": { + "Allocation": { + "source": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "target": "09342ccb-044b-5483-ac58-87dc3cf7831c" + } + }, + "description": "", + "id": "3cc9ab52-2b00-57fd-8709-aabc75e5d3e3", + "name": "Allocation: Reset valve remotely" + }, + "3f54d890-4ca5-5e36-873b-abb5c92d578c": { + "alias": "oa_request_then_repair", + "body": { + "SequenceLink": { + "guard": "", + "source": "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "target": "c59432fe-6092-5ee0-908d-9dbf39476471" + } + }, + "description": "", + "id": "3f54d890-4ca5-5e36-873b-abb5c92d578c", + "name": "Sequence link: Repair the valve" + }, + "4549d3fa-5ab9-52a9-b4f9-3a78fc9d625a": { + "alias": "oa_report_carriage", + "body": { + "ExchangeAllocation": { + "source": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "target": "b436d0b3-c824-5a96-af6b-2c269816d1cc" + } + }, + "description": "", + "id": "4549d3fa-5ab9-52a9-b4f9-3a78fc9d625a", + "name": "Exchange allocation: Report repair result" + }, + "47730532-0372-5cc5-9d6d-0b8953aebdd8": { + "alias": "sa_start_monitoring", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "", + "source": "57eb5348-14d8-560e-996a-8dbf993243e2", + "target": "8af21f9b-c21e-5aae-af0d-0958840f6655", + "transition_kind": "External", + "trigger": "", + "triggers": [] + } + }, + "description": "", + "id": "47730532-0372-5cc5-9d6d-0b8953aebdd8", + "name": "State transition: Monitoring" + }, + "4c4c4851-5a3d-5bb9-94df-b19f9cf39819": { + "alias": "sa_request_carriage", + "body": { + "ExchangeAllocation": { + "source": "63009b5f-539e-5914-b630-ec6b7fefb4d5", + "target": "93474111-f23e-56c1-b61b-b7c2396b91bd" + } + }, + "description": "", + "id": "4c4c4851-5a3d-5bb9-94df-b19f9cf39819", + "name": "Exchange allocation: Request field repair" + }, + "4f85337b-f6eb-533c-a45a-9a5d1d9567ca": { + "alias": "sa_client_requires", + "body": { + "InterfaceRequirement": { + "source": "98d411e7-7c65-5c3f-b543-8bf0f2522ea2", + "target": "a4b8942e-a975-5f88-ad14-8f38c345baac" + } + }, + "description": "", + "id": "4f85337b-f6eb-533c-a45a-9a5d1d9567ca", + "name": "Interface requirement: Valve repair service" + }, + "55ec4dbf-bd17-5b0c-b3a3-95b2194aa8b9": { + "alias": "sa_port_request_out", + "body": { + "PortAllocation": { + "source": "98d411e7-7c65-5c3f-b543-8bf0f2522ea2", + "target": "24a6850e-b819-57e0-af88-ca4ab76581b7" + } + }, + "description": "", + "id": "55ec4dbf-bd17-5b0c-b3a3-95b2194aa8b9", + "name": "Port allocation: Request field repair output" + }, + "55f89fbf-aa3c-5ae1-95ef-ac676f9c1ada": { + "alias": "sa_provider_provides", + "body": { + "InterfaceProvision": { + "source": "e6d69cbd-99d7-596b-bbe3-dada6118a600", + "target": "a4b8942e-a975-5f88-ad14-8f38c345baac" + } + }, + "description": "", + "id": "55f89fbf-aa3c-5ae1-95ef-ac676f9c1ada", + "name": "Interface provision: Valve repair service" + }, + "568c5c01-de21-52a2-83bd-afdddc158085": { + "alias": "sa_port_report_in", + "body": { + "PortAllocation": { + "source": "98d411e7-7c65-5c3f-b543-8bf0f2522ea2", + "target": "391af7c4-b07d-53a7-8ddd-5c81461fc4f8" + } + }, + "description": "", + "id": "568c5c01-de21-52a2-83bd-afdddc158085", + "name": "Port allocation: Report repair result input" + }, + "594c61cd-94e0-5c07-890d-edfd520d7df9": { + "alias": "oa_assess_allocation", + "body": { + "Allocation": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "61f7bbfb-74a4-5540-9182-2583dfb27a49" + } + }, + "description": "", + "id": "594c61cd-94e0-5c07-890d-edfd520d7df9", + "name": "Allocation: Assess pressure alarm" + }, + "628cb4fc-fe63-5f6b-b364-6268f26b5740": { + "alias": "oa_assessment_delivery", + "body": { + "ChainInvolvement": { + "source": "022d3cfa-bbaf-5e13-924a-8e55099b8745", + "target": "6a30b763-cc51-546d-907f-5c63657cfd03" + } + }, + "description": "", + "id": "628cb4fc-fe63-5f6b-b364-6268f26b5740", + "name": "Chain involvement: Assess an alarm" + }, + "63009b5f-539e-5914-b630-ec6b7fefb4d5": { + "alias": "sa_contact", + "body": { + "BoundaryExchange": { + "items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ], + "oriented": false, + "source": "98d411e7-7c65-5c3f-b543-8bf0f2522ea2", + "target": "e6d69cbd-99d7-596b-bbe3-dada6118a600" + } + }, + "description": "", + "id": "63009b5f-539e-5914-b630-ec6b7fefb4d5", + "name": "Technician service conversation" + }, + "66efc484-53c7-51a3-89e9-422429344b38": { + "alias": "sa_response_delivery", + "body": { + "ChainInvolvement": { + "source": "5139ce73-56fd-5875-ba3a-e569fc890231", + "target": "81259051-370a-512d-a3b5-6420a6f933b6" + } + }, + "description": "", + "id": "66efc484-53c7-51a3-89e9-422429344b38", + "name": "Chain involvement: Restore a safe supply" + }, + "684eadc3-e670-5779-a60f-bbc9a5bfb062": { + "alias": "sa_repair_report", + "body": { + "FunctionalExchange": { + "items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ], + "source": "9497e10f-04c4-5586-8738-2464a6f00bd4", + "source_port": "994faed7-dbc8-5b96-9d62-7604ac0fbc3a", + "target": "d75eb3fa-a27d-52d5-8782-8b342b3865f6", + "target_port": "391af7c4-b07d-53a7-8ddd-5c81461fc4f8" + } + }, + "description": "", + "id": "684eadc3-e670-5779-a60f-bbc9a5bfb062", + "name": "Report repair result" + }, + "6a6d968f-26d6-5dbb-b721-5a27d362779a": { + "alias": "sa_request_then_repair", + "body": { + "SequenceLink": { + "guard": "", + "source": "40d94322-baa2-5c81-b261-ff47cdc33782", + "target": "0f195fea-3493-5ebd-966d-da9eddeb05d7" + } + }, + "description": "", + "id": "6a6d968f-26d6-5dbb-b721-5a27d362779a", + "name": "Sequence link: Repair the valve" + }, + "6e2c1b47-764c-577a-b325-ff66dd2a2768": { + "alias": "sa_normal_transition", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "repair confirmed and pressure is safe", + "source": "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37", + "target": "8af21f9b-c21e-5aae-af0d-0958840f6655", + "transition_kind": "External", + "trigger": "", + "triggers": [] + } + }, + "description": "", + "id": "6e2c1b47-764c-577a-b325-ff66dd2a2768", + "name": "State transition: Monitoring" + }, + "725f2e1b-aa2c-5615-9b1f-28da178168fa": { + "alias": "sa_repair_allocation", + "body": { + "Allocation": { + "source": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a", + "target": "9497e10f-04c4-5586-8738-2464a6f00bd4" + } + }, + "description": "", + "id": "725f2e1b-aa2c-5615-9b1f-28da178168fa", + "name": "Allocation: Repair valve in the field" + }, + "76218d89-68fd-531d-b35b-90c5b769a5ef": { + "alias": "oa_remote_path", + "body": { + "SequenceLink": { + "guard": "remote reset is authorized", + "source": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "target": "d2026fc2-74a8-5e22-9942-ff3eb68a3677" + } + }, + "description": "", + "id": "76218d89-68fd-531d-b35b-90c5b769a5ef", + "name": "remote reset is authorized" + }, + "7fa37e63-db65-5aa7-88f2-31ff4347a787": { + "alias": "oa_assess_on_entry", + "body": { + "StateAction": { + "phase": "Entry", + "source": "58c15833-6e75-523a-b57f-a88f19f6d672", + "target": "61f7bbfb-74a4-5540-9182-2583dfb27a49" + } + }, + "description": "", + "id": "7fa37e63-db65-5aa7-88f2-31ff4347a787", + "name": "Assess when repair is needed" + }, + "7fb30b1d-195c-5f0e-a19c-a536f519a899": { + "alias": "sa_repair_then_report", + "body": { + "SequenceLink": { + "guard": "", + "source": "0f195fea-3493-5ebd-966d-da9eddeb05d7", + "target": "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac" + } + }, + "description": "", + "id": "7fb30b1d-195c-5f0e-a19c-a536f519a899", + "name": "Sequence link: Report the measured result" + }, + "93474111-f23e-56c1-b61b-b7c2396b91bd": { + "alias": "sa_repair_request", + "body": { + "FunctionalExchange": { + "items": [ + "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + ], + "source": "dddae020-a444-5d48-acc8-139d2696a672", + "source_port": "24a6850e-b819-57e0-af88-ca4ab76581b7", + "target": "9497e10f-04c4-5586-8738-2464a6f00bd4", + "target_port": "d2113fb0-a531-54ef-9484-13951b00ab8f" + } + }, + "description": "", + "id": "93474111-f23e-56c1-b61b-b7c2396b91bd", + "name": "Request field repair" + }, + "93eaad8d-83b7-5980-b9bb-86c9e3f31da4": { + "alias": "sa_assessment_support", + "body": { + "CapabilityInvolvement": { + "source": "dddae020-a444-5d48-acc8-139d2696a672", + "target": "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f" + } + }, + "description": "", + "id": "93eaad8d-83b7-5980-b9bb-86c9e3f31da4", + "name": "Capability involvement: Assess an alarm" + }, + "9b9356d7-2db2-5a94-a47e-ea892cf02afe": { + "alias": "sa_assess_allocation", + "body": { + "Allocation": { + "source": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "target": "dddae020-a444-5d48-acc8-139d2696a672" + } + }, + "description": "", + "id": "9b9356d7-2db2-5a94-a47e-ea892cf02afe", + "name": "Allocation: Assess pressure alarm" + }, + "9d4a131f-36ad-5125-b21f-c3dd033b8c82": { + "alias": "sa_assess_then_decide", + "body": { + "SequenceLink": { + "guard": "", + "source": "9024243c-ccc1-5edb-8477-2aa16e5e9a50", + "target": "30c8fad9-a49b-5692-9542-3148e76afd7b" + } + }, + "description": "", + "id": "9d4a131f-36ad-5125-b21f-c3dd033b8c82", + "name": "Continue" + }, + "a0902e0f-3d18-576a-9aca-975a136dfdd7": { + "alias": "sa_mission_delivery", + "body": { + "MissionCapability": { + "source": "5ad034b1-084d-5273-9e70-971671766132", + "target": "81259051-370a-512d-a3b5-6420a6f933b6" + } + }, + "description": "", + "id": "a0902e0f-3d18-576a-9aca-975a136dfdd7", + "name": "Mission capability: Restore a safe supply" + }, + "a6d5a6be-fa6e-5fa0-a309-08e87ba40c57": { + "alias": "sa_assessment_delivery", + "body": { + "ChainInvolvement": { + "source": "acd5a018-f559-5298-a40d-1cf6e1353e74", + "target": "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f" + } + }, + "description": "", + "id": "a6d5a6be-fa6e-5fa0-a309-08e87ba40c57", + "name": "Chain involvement: Assess an alarm" + }, + "a887f96a-37cb-5891-8070-011fa276e5b5": { + "alias": "sa_assess_on_entry", + "body": { + "StateAction": { + "phase": "Entry", + "source": "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37", + "target": "dddae020-a444-5d48-acc8-139d2696a672" + } + }, + "description": "", + "id": "a887f96a-37cb-5891-8070-011fa276e5b5", + "name": "Assess when repair is needed" + }, + "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2": { + "alias": "oa_repair_allocation", + "body": { + "Allocation": { + "source": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "target": "a21465af-05f3-5fb5-8983-de26932422fb" + } + }, + "description": "", + "id": "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2", + "name": "Allocation: Repair valve in the field" + }, + "b436d0b3-c824-5a96-af6b-2c269816d1cc": { + "alias": "oa_repair_report", + "body": { + "Interaction": { + "items": [ + "d736c7d6-d369-550a-8ba4-dae223e40bb3" + ], + "source": "a21465af-05f3-5fb5-8983-de26932422fb", + "target": "727c0b5c-8db6-5eb7-83d6-735519c5949f" + } + }, + "description": "", + "id": "b436d0b3-c824-5a96-af6b-2c269816d1cc", + "name": "Report repair result" + }, + "b76bf8de-2fa4-563f-a540-dd30f30e6c49": { + "alias": "sa_confirm_allocation", + "body": { + "Allocation": { + "source": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "target": "d75eb3fa-a27d-52d5-8782-8b342b3865f6" + } + }, + "description": "", + "id": "b76bf8de-2fa4-563f-a540-dd30f30e6c49", + "name": "Allocation: Confirm safe pressure" + }, + "bac43d3b-910b-5bdc-be9b-f78cf973e1ed": { + "alias": "oa_remote_merge", + "body": { + "SequenceLink": { + "guard": "", + "source": "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "target": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d" + } + }, + "description": "", + "id": "bac43d3b-910b-5bdc-be9b-f78cf973e1ed", + "name": "Continue" + }, + "bfd4326e-ea65-5269-b5e6-e063a355d1e9": { + "alias": "oa_confirm_allocation", + "body": { + "Allocation": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "727c0b5c-8db6-5eb7-83d6-735519c5949f" + } + }, + "description": "", + "id": "bfd4326e-ea65-5269-b5e6-e063a355d1e9", + "name": "Allocation: Confirm safe pressure" + }, + "c1adbbd0-a762-5ee3-8b9a-5c18af377771": { + "alias": "oa_field_path", + "body": { + "SequenceLink": { + "guard": "else", + "source": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "target": "fc6763bf-a534-54bf-afcd-57ab285ff07d" + } + }, + "description": "", + "id": "c1adbbd0-a762-5ee3-8b9a-5c18af377771", + "name": "else" + }, + "c7c248f8-28b8-55be-9023-0991d44021e3": { + "alias": "oa_response_delivery", + "body": { + "ChainInvolvement": { + "source": "58bae2b0-f560-50f2-818d-19091d22d18a", + "target": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb" + } + }, + "description": "", + "id": "c7c248f8-28b8-55be-9023-0991d44021e3", + "name": "Chain involvement: Restore a safe supply" + }, + "c8196d35-2168-5fef-8b03-ba0d139bc00f": { + "alias": "oa_repair_then_report", + "body": { + "SequenceLink": { + "guard": "", + "source": "c59432fe-6092-5ee0-908d-9dbf39476471", + "target": "df2fd3f8-6bc2-5380-a13f-36643ed35ac7" + } + }, + "description": "", + "id": "c8196d35-2168-5fef-8b03-ba0d139bc00f", + "name": "Sequence link: Report the measured result" + }, + "c8593670-8906-530f-b6a2-3ada77a9c219": { + "alias": "oa_field_merge", + "body": { + "SequenceLink": { + "guard": "", + "source": "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "target": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d" + } + }, + "description": "", + "id": "c8593670-8906-530f-b6a2-3ada77a9c219", + "name": "Continue" + }, + "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d": { + "alias": "oa_repair_request", + "body": { + "Interaction": { + "items": [ + "d736c7d6-d369-550a-8ba4-dae223e40bb3" + ], + "source": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "target": "a21465af-05f3-5fb5-8983-de26932422fb" + } + }, + "description": "", + "id": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "name": "Request field repair" + }, + "d8b6710a-e713-5979-934f-e5ca7d155bb4": { + "alias": "sa_remote_path", + "body": { + "SequenceLink": { + "guard": "remote reset is authorized", + "source": "30c8fad9-a49b-5692-9542-3148e76afd7b", + "target": "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6" + } + }, + "description": "", + "id": "d8b6710a-e713-5979-934f-e5ca7d155bb4", + "name": "remote reset is authorized" + }, + "e21d9a54-ae8e-5618-a4f8-67ee22b92638": { + "alias": "oa_include_triage", + "body": { + "CapabilityInclude": { + "source": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "target": "6a30b763-cc51-546d-907f-5c63657cfd03" + } + }, + "description": "", + "id": "e21d9a54-ae8e-5618-a4f8-67ee22b92638", + "name": "Capability include: Assess an alarm" + }, + "e5279265-8eac-52e8-98dc-571b20475e1f": { + "alias": "sa_remote_merge", + "body": { + "SequenceLink": { + "guard": "", + "source": "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6", + "target": "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07" + } + }, + "description": "", + "id": "e5279265-8eac-52e8-98dc-571b20475e1f", + "name": "Continue" + }, + "e53b7a40-e8a3-5b3f-826d-c2b5c164a84a": { + "alias": "sa_port_report_out", + "body": { + "PortAllocation": { + "source": "e6d69cbd-99d7-596b-bbe3-dada6118a600", + "target": "994faed7-dbc8-5b96-9d62-7604ac0fbc3a" + } + }, + "description": "", + "id": "e53b7a40-e8a3-5b3f-826d-c2b5c164a84a", + "name": "Port allocation: Report repair result output" + }, + "f074e041-23cf-54ad-af9c-f171e30621e7": { + "alias": "sa_repair_interface_item", + "body": { + "InterfaceItemAllocation": { + "acquisition": "Accept", + "order": 0, + "source": "a4b8942e-a975-5f88-ad14-8f38c345baac", + "target": "a962fc5e-3ab0-5296-8b0d-fd84633f9218", + "transmission": "Synchronous" + } + }, + "description": "", + "id": "f074e041-23cf-54ad-af9c-f171e30621e7", + "name": "Repair operation contract" + }, + "f3fe7690-734a-5877-8b57-a6599a4033c4": { + "alias": "sa_include_triage", + "body": { + "CapabilityInclude": { + "source": "81259051-370a-512d-a3b5-6420a6f933b6", + "target": "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f" + } + }, + "description": "", + "id": "f3fe7690-734a-5877-8b57-a6599a4033c4", + "name": "Capability include: Assess an alarm" + }, + "fbde9d5e-c7e9-55db-a2be-497345c491b3": { + "alias": "oa_alarm_transition", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "", + "source": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "target": "58c15833-6e75-523a-b57f-a88f19f6d672", + "transition_kind": "External", + "trigger": "", + "triggers": [ + "65d6b8d6-271a-5c84-8b42-abbe310c955e" + ] + } + }, + "description": "", + "id": "fbde9d5e-c7e9-55db-a2be-497345c491b3", + "name": "State transition: Awaiting repair" + }, + "ff9897b3-a23a-5994-a5b3-99a5b4d34ceb": { + "alias": "sa_report_carriage", + "body": { + "ExchangeAllocation": { + "source": "63009b5f-539e-5914-b630-ec6b7fefb4d5", + "target": "684eadc3-e670-5779-a60f-bbc9a5bfb062" + } + }, + "description": "", + "id": "ff9897b3-a23a-5994-a5b3-99a5b4d34ceb", + "name": "Exchange allocation: Report repair result" + }, + "ffdbfcd0-d423-5707-a880-0dfbe1400f72": { + "alias": "oa_assessment_support", + "body": { + "CapabilityInvolvement": { + "source": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "target": "6a30b763-cc51-546d-907f-5c63657cfd03" + } + }, + "description": "", + "id": "ffdbfcd0-d423-5707-a880-0dfbe1400f72", + "name": "Capability involvement: Assess an alarm" + } + }, + "schema_version": 6, + "traces": { + "0754be23-e4b8-53bc-b03c-89c2dbd80361": { + "alias": "trace_oa_waiting", + "baseline": { + "oa": { + "description": "", + "name": "Awaiting repair" + }, + "sa": { + "description": "", + "name": "Awaiting repair" + } + }, + "description": "", + "id": "0754be23-e4b8-53bc-b03c-89c2dbd80361", + "kind": "State", + "name": "Awaiting repair ↔ Awaiting repair", + "oa": "58c15833-6e75-523a-b57f-a88f19f6d672", + "sa": "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37" + }, + "07a7b82a-b5a7-5737-abfa-09e422f7420e": { + "alias": "trace_oa_start_monitoring", + "baseline": { + "oa": { + "description": "", + "name": "State transition: Monitoring" + }, + "sa": { + "description": "", + "name": "State transition: Monitoring" + } + }, + "description": "", + "id": "07a7b82a-b5a7-5737-abfa-09e422f7420e", + "kind": "Manual", + "name": "State transition: Monitoring ↔ State transition: Monitoring", + "oa": "2b00270c-79a5-52a5-af96-33346bea531b", + "sa": "47730532-0372-5cc5-9d6d-0b8953aebdd8" + }, + "0c7c8a06-163e-5b3e-8a98-e66e2ce8963b": { + "alias": "trace_oa_alarm_transition", + "baseline": { + "oa": { + "description": "", + "name": "State transition: Awaiting repair" + }, + "sa": { + "description": "", + "name": "State transition: Awaiting repair" + } + }, + "description": "", + "id": "0c7c8a06-163e-5b3e-8a98-e66e2ce8963b", + "kind": "Manual", + "name": "State transition: Awaiting repair ↔ State transition: Awaiting repair", + "oa": "fbde9d5e-c7e9-55db-a2be-497345c491b3", + "sa": "13c3bfa4-9254-5ab2-b96f-f65f402adf81" + }, + "0c9de959-d366-5765-98eb-450ea27bd316": { + "alias": "trace_oa_merge", + "baseline": { + "oa": { + "description": "", + "name": "Recovery path completed" + }, + "sa": { + "description": "", + "name": "Recovery path completed" + } + }, + "description": "", + "id": "0c9de959-d366-5765-98eb-450ea27bd316", + "kind": "Sequence", + "name": "Recovery path completed ↔ Recovery path completed", + "oa": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "sa": "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07" + }, + "0d905fbb-5b3c-56f0-9f11-a27091901eb0": { + "alias": "trace_oa_pressure_parameter", + "baseline": { + "oa": { + "description": "", + "name": "Pressure before / after repair" + }, + "sa": { + "description": "", + "name": "Pressure before / after repair" + } + }, + "description": "", + "id": "0d905fbb-5b3c-56f0-9f11-a27091901eb0", + "kind": "Item", + "name": "Pressure before / after repair ↔ Pressure before / after repair", + "oa": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "sa": "bb12ba5d-aab5-5a1b-99de-c767f5365b57" + }, + "0fbef164-b5d3-58ba-ba41-54d0e63709ab": { + "alias": "trace_oa_restore", + "baseline": { + "oa": { + "description": "", + "name": "Restore a safe supply" + }, + "sa": { + "description": "", + "name": "Restore a safe supply" + } + }, + "description": "", + "id": "0fbef164-b5d3-58ba-ba41-54d0e63709ab", + "kind": "Capability", + "name": "Restore a safe supply ↔ Restore a safe supply", + "oa": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "sa": "81259051-370a-512d-a3b5-6420a6f933b6" + }, + "12ec320f-2e14-5a5a-8c6b-bd2b6471dae9": { + "alias": "trace_oa_monitoring", + "baseline": { + "oa": { + "description": "", + "name": "Monitoring" + }, + "sa": { + "description": "", + "name": "Monitoring" + } + }, + "description": "", + "id": "12ec320f-2e14-5a5a-8c6b-bd2b6471dae9", + "kind": "State", + "name": "Monitoring ↔ Monitoring", + "oa": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "sa": "8af21f9b-c21e-5aae-af0d-0958840f6655" + }, + "154abfce-c311-52d1-85d1-3f4c85203b26": { + "alias": "trace_oa_confirm_step", + "baseline": { + "oa": { + "description": "", + "name": "Confirm safe pressure" + }, + "sa": { + "description": "", + "name": "Confirm safe pressure" + } + }, + "description": "", + "id": "154abfce-c311-52d1-85d1-3f4c85203b26", + "kind": "Sequence", + "name": "Confirm safe pressure ↔ Confirm safe pressure", + "oa": "e00594a3-6122-5a6b-b3c2-ecb5506c741d", + "sa": "47bec4f9-d6d0-5ed1-adca-fa0d960dc416" + }, + "1b788e74-36b2-5c17-a936-642dbe51ca3c": { + "alias": "trace_oa_remote_merge", + "baseline": { + "oa": { + "description": "", + "name": "Continue" + }, + "sa": { + "description": "", + "name": "Continue" + } + }, + "description": "", + "id": "1b788e74-36b2-5c17-a936-642dbe51ca3c", + "kind": "Manual", + "name": "Continue ↔ Continue", + "oa": "bac43d3b-910b-5bdc-be9b-f78cf973e1ed", + "sa": "e5279265-8eac-52e8-98dc-571b20475e1f" + }, + "208753e0-9151-50d8-86ca-29fc28eca80b": { + "alias": "trace_oa_monitor_region", + "baseline": { + "oa": { + "description": "", + "name": "Supervision cycle" + }, + "sa": { + "description": "", + "name": "Supervision cycle" + } + }, + "description": "", + "id": "208753e0-9151-50d8-86ca-29fc28eca80b", + "kind": "State", + "name": "Supervision cycle ↔ Supervision cycle", + "oa": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7", + "sa": "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c" + }, + "241dfdac-2450-5704-a4cb-9f1047db1b6d": { + "alias": "trace_oa_caller", + "baseline": { + "oa": { + "description": "", + "name": "Controller" + }, + "sa": { + "description": "", + "name": "Controller" + } + }, + "description": "", + "id": "241dfdac-2450-5704-a4cb-9f1047db1b6d", + "kind": "Scenario", + "name": "Controller ↔ Controller", + "oa": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "sa": "98c61788-2fee-51f4-8cf9-de48e8c70e9e" + }, + "2e849e31-35aa-5aef-a989-26948038e52b": { + "alias": "trace_oa_field_path", + "baseline": { + "oa": { + "description": "", + "name": "else" + }, + "sa": { + "description": "", + "name": "else" + } + }, + "description": "", + "id": "2e849e31-35aa-5aef-a989-26948038e52b", + "kind": "Manual", + "name": "else ↔ else", + "oa": "c1adbbd0-a762-5ee3-8b9a-5c18af377771", + "sa": "1bbd5957-1173-51d8-97e3-ba4f084227ae" + }, + "2fdcbc6f-526e-5a4e-bde5-314cf0cf3ef3": { + "alias": "trace_oa_confirm_allocation", + "baseline": { + "oa": { + "description": "", + "name": "Allocation: Confirm safe pressure" + }, + "sa": { + "description": "", + "name": "Allocation: Confirm safe pressure" + } + }, + "description": "", + "id": "2fdcbc6f-526e-5a4e-bde5-314cf0cf3ef3", + "kind": "Manual", + "name": "Allocation: Confirm safe pressure ↔ Allocation: Confirm safe pressure", + "oa": "bfd4326e-ea65-5269-b5e6-e063a355d1e9", + "sa": "b76bf8de-2fa4-563f-a540-dd30f30e6c49" + }, + "3694c752-ba80-5702-a7d7-9cc66b6b01d8": { + "alias": "trace_oa_conversation", + "baseline": { + "oa": { + "description": "", + "name": "Repair request and measured reply" + }, + "sa": { + "description": "", + "name": "Repair request and measured reply" + } + }, + "description": "", + "id": "3694c752-ba80-5702-a7d7-9cc66b6b01d8", + "kind": "Scenario", + "name": "Repair request and measured reply ↔ Repair request and measured reply", + "oa": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423", + "sa": "730b27ca-b039-5a3b-8dc6-03418f9061b3" + }, + "3714d547-006f-5c3a-97f8-175ccc23d994": { + "alias": "trace_oa_repair_allocation", + "baseline": { + "oa": { + "description": "", + "name": "Allocation: Repair valve in the field" + }, + "sa": { + "description": "", + "name": "Allocation: Repair valve in the field" + } + }, + "description": "", + "id": "3714d547-006f-5c3a-97f8-175ccc23d994", + "kind": "Manual", + "name": "Allocation: Repair valve in the field ↔ Allocation: Repair valve in the field", + "oa": "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2", + "sa": "725f2e1b-aa2c-5615-9b1f-28da178168fa" + }, + "3a33de40-20a3-5252-899c-c12dc35e9783": { + "alias": "trace_oa_assessment_action", + "baseline": { + "oa": { + "description": "", + "name": "Assess pressure alarm" + }, + "sa": { + "description": "", + "name": "Assess pressure alarm" + } + }, + "description": "", + "id": "3a33de40-20a3-5252-899c-c12dc35e9783", + "kind": "Sequence", + "name": "Assess pressure alarm ↔ Assess pressure alarm", + "oa": "942975b8-0bef-5ae8-9fcb-c37497833fb0", + "sa": "bae5b192-5b8e-51c6-82ae-2331af0d33bf" + }, + "3e7bde54-f9c1-549f-9e5b-2ce1ecb685a7": { + "alias": "trace_oa_repair_traffic", + "baseline": { + "oa": { + "description": "", + "name": "Repair work exchanges" + }, + "sa": { + "description": "", + "name": "Repair work exchanges" + } + }, + "description": "", + "id": "3e7bde54-f9c1-549f-9e5b-2ce1ecb685a7", + "kind": "Manual", + "name": "Repair work exchanges ↔ Repair work exchanges", + "oa": "9349b353-53f2-56f8-b571-a05b09335cdf", + "sa": "a9a47e48-100c-57a8-95f0-7fe24da94f94" + }, + "424eca39-1c29-5a2f-b9ee-33dd018926aa": { + "alias": "trace_oa_repair_report", + "baseline": { + "oa": { + "description": "", + "name": "Report repair result" + }, + "sa": { + "description": "", + "name": "Report repair result" + } + }, + "description": "", + "id": "424eca39-1c29-5a2f-b9ee-33dd018926aa", + "kind": "Exchange", + "name": "Report repair result ↔ Report repair result", + "oa": "b436d0b3-c824-5a96-af6b-2c269816d1cc", + "sa": "684eadc3-e670-5779-a60f-bbc9a5bfb062" + }, + "435bae4c-cf6d-59f3-9eb5-d660ebeec079": { + "alias": "trace_oa_full_scenario", + "baseline": { + "oa": { + "description": "", + "name": "Restore supply through field repair" + }, + "sa": { + "description": "", + "name": "Restore supply through field repair" + } + }, + "description": "", + "id": "435bae4c-cf6d-59f3-9eb5-d660ebeec079", + "kind": "Scenario", + "name": "Restore supply through field repair ↔ Restore supply through field repair", + "oa": "d6f881a4-95da-54e9-ad97-1b8965532629", + "sa": "f746a1cd-ff9f-55c0-b325-01c03d2f4fae" + }, + "48d243f0-553f-507e-acb0-9474ae156b63": { + "alias": "trace_oa_monitor_initial", + "baseline": { + "oa": { + "description": "", + "name": "Start" + }, + "sa": { + "description": "", + "name": "Start" + } + }, + "description": "", + "id": "48d243f0-553f-507e-acb0-9474ae156b63", + "kind": "State", + "name": "Start ↔ Start", + "oa": "a7ffb869-9dde-5e76-b276-c719c5c143a9", + "sa": "57eb5348-14d8-560e-996a-8dbf993243e2" + }, + "5309f955-53fd-5ff4-9c0a-91737e947341": { + "alias": "trace_oa_assess", + "baseline": { + "oa": { + "description": "", + "name": "Assess pressure alarm" + }, + "sa": { + "description": "", + "name": "Assess pressure alarm" + } + }, + "description": "", + "id": "5309f955-53fd-5ff4-9c0a-91737e947341", + "kind": "Refinement", + "name": "Assess pressure alarm ↔ Assess pressure alarm", + "oa": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "sa": "dddae020-a444-5d48-acc8-139d2696a672" + }, + "595b5729-ccd9-5291-b5d9-30d8c8bd9dfa": { + "alias": "trace_oa_reply", + "baseline": { + "oa": { + "description": "", + "name": "Return pressure after repair" + }, + "sa": { + "description": "", + "name": "Return pressure after repair" + } + }, + "description": "", + "id": "595b5729-ccd9-5291-b5d9-30d8c8bd9dfa", + "kind": "Scenario", + "name": "Return pressure after repair ↔ Return pressure after repair", + "oa": "cc2d497a-fd15-5455-9ee5-e70eb95811d8", + "sa": "4e1c0a56-ce80-5f13-a93a-6956de54880c" + }, + "5b96bfed-63e2-5d7f-b882-91e8b81f8efe": { + "alias": "trace_oa_assessment_delivery", + "baseline": { + "oa": { + "description": "", + "name": "Chain involvement: Assess an alarm" + }, + "sa": { + "description": "", + "name": "Chain involvement: Assess an alarm" + } + }, + "description": "", + "id": "5b96bfed-63e2-5d7f-b882-91e8b81f8efe", + "kind": "Manual", + "name": "Chain involvement: Assess an alarm ↔ Chain involvement: Assess an alarm", + "oa": "628cb4fc-fe63-5f6b-b364-6268f26b5740", + "sa": "a6d5a6be-fa6e-5fa0-a309-08e87ba40c57" + }, + "6acd072d-431e-5881-ab20-1813b4fda0c5": { + "alias": "trace_oa_triage", + "baseline": { + "oa": { + "description": "", + "name": "Assess an alarm" + }, + "sa": { + "description": "", + "name": "Assess an alarm" + } + }, + "description": "", + "id": "6acd072d-431e-5881-ab20-1813b4fda0c5", + "kind": "Capability", + "name": "Assess an alarm ↔ Assess an alarm", + "oa": "6a30b763-cc51-546d-907f-5c63657cfd03", + "sa": "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f" + }, + "6ca4c78a-a5f2-5470-b401-732f5a8825d0": { + "alias": "trace_oa_assessment_procedure", + "baseline": { + "oa": { + "description": "", + "name": "Assess an alarm procedure" + }, + "sa": { + "description": "", + "name": "Assess an alarm procedure" + } + }, + "description": "", + "id": "6ca4c78a-a5f2-5470-b401-732f5a8825d0", + "kind": "Sequence", + "name": "Assess an alarm procedure ↔ Assess an alarm procedure", + "oa": "022d3cfa-bbaf-5e13-924a-8e55099b8745", + "sa": "acd5a018-f559-5298-a40d-1cf6e1353e74" + }, + "6e13cf86-7242-5e33-8912-b037cf6e8c87": { + "alias": "trace_oa_assessment_support", + "baseline": { + "oa": { + "description": "", + "name": "Capability involvement: Assess an alarm" + }, + "sa": { + "description": "", + "name": "Capability involvement: Assess an alarm" + } + }, + "description": "", + "id": "6e13cf86-7242-5e33-8912-b037cf6e8c87", + "kind": "Manual", + "name": "Capability involvement: Assess an alarm ↔ Capability involvement: Assess an alarm", + "oa": "ffdbfcd0-d423-5707-a880-0dfbe1400f72", + "sa": "93eaad8d-83b7-5980-b9bb-86c9e3f31da4" + }, + "70c90b0c-85ce-574a-a217-f0d19fad30e1": { + "alias": "trace_oa_response_delivery", + "baseline": { + "oa": { + "description": "", + "name": "Chain involvement: Restore a safe supply" + }, + "sa": { + "description": "", + "name": "Chain involvement: Restore a safe supply" + } + }, + "description": "", + "id": "70c90b0c-85ce-574a-a217-f0d19fad30e1", + "kind": "Manual", + "name": "Chain involvement: Restore a safe supply ↔ Chain involvement: Restore a safe supply", + "oa": "c7c248f8-28b8-55be-9023-0991d44021e3", + "sa": "66efc484-53c7-51a3-89e9-422429344b38" + }, + "77073fcb-53aa-5f06-987a-0317cbb6971d": { + "alias": "trace_oa_include_triage", + "baseline": { + "oa": { + "description": "", + "name": "Capability include: Assess an alarm" + }, + "sa": { + "description": "", + "name": "Capability include: Assess an alarm" + } + }, + "description": "", + "id": "77073fcb-53aa-5f06-987a-0317cbb6971d", + "kind": "Manual", + "name": "Capability include: Assess an alarm ↔ Capability include: Assess an alarm", + "oa": "e21d9a54-ae8e-5618-a4f8-67ee22b92638", + "sa": "f3fe7690-734a-5877-8b57-a6599a4033c4" + }, + "7a6b4b9c-4220-546a-a752-1587c2881fe1": { + "alias": "trace_oa_use_field", + "baseline": { + "oa": { + "description": "", + "name": "Use the field repair procedure" + }, + "sa": { + "description": "", + "name": "Use the field repair procedure" + } + }, + "description": "", + "id": "7a6b4b9c-4220-546a-a752-1587c2881fe1", + "kind": "Sequence", + "name": "Use the field repair procedure ↔ Use the field repair procedure", + "oa": "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "sa": "d5e61c5d-510b-5091-a878-da46f3a9e152" + }, + "82a5be1a-92a8-578d-9448-086062a20d8f": { + "alias": "trace_oa_reset", + "baseline": { + "oa": { + "description": "", + "name": "Reset valve remotely" + }, + "sa": { + "description": "", + "name": "Reset valve remotely" + } + }, + "description": "", + "id": "82a5be1a-92a8-578d-9448-086062a20d8f", + "kind": "Refinement", + "name": "Reset valve remotely ↔ Reset valve remotely", + "oa": "d0c8be87-d238-503a-a549-7558a210507f", + "sa": "09342ccb-044b-5483-ac58-87dc3cf7831c" + }, + "82cca563-9d5d-5479-aa1a-9db03e9674d1": { + "alias": "trace_oa_restore_work", + "baseline": { + "oa": { + "description": "", + "name": "Restore safe pressure" + }, + "sa": { + "description": "", + "name": "Restore safe pressure" + } + }, + "description": "", + "id": "82cca563-9d5d-5479-aa1a-9db03e9674d1", + "kind": "Refinement", + "name": "Restore safe pressure ↔ Restore safe pressure", + "oa": "1b91f8a7-0bdb-514e-b1cd-955658e09927", + "sa": "58867e7d-916e-5ebc-9e4c-8dbed1314be3" + }, + "8d693d90-5017-56af-b056-d29d3cb46baa": { + "alias": "trace_oa_mission_participant", + "baseline": { + "oa": { + "description": "", + "name": "Mission involvement: Maintain safe water supply" + }, + "sa": { + "description": "", + "name": "Mission involvement: Maintain safe water supply" + } + }, + "description": "", + "id": "8d693d90-5017-56af-b056-d29d3cb46baa", + "kind": "Manual", + "name": "Mission involvement: Maintain safe water supply ↔ Mission involvement: Maintain safe water supply", + "oa": "0c4c9e58-ff32-5c00-b82a-d408a4be0183", + "sa": "1c1e5634-ba8c-591f-b143-90a27cdc6cd2" + }, + "98ef05b5-5381-5d35-a433-ebab55e9f382": { + "alias": "trace_oa_reset_allocation", + "baseline": { + "oa": { + "description": "", + "name": "Allocation: Reset valve remotely" + }, + "sa": { + "description": "", + "name": "Allocation: Reset valve remotely" + } + }, + "description": "", + "id": "98ef05b5-5381-5d35-a433-ebab55e9f382", + "kind": "Manual", + "name": "Allocation: Reset valve remotely ↔ Allocation: Reset valve remotely", + "oa": "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a", + "sa": "3cc9ab52-2b00-57fd-8709-aabc75e5d3e3" + }, + "a106d183-c239-5c9f-b1f1-141af19fc778": { + "alias": "trace_oa_confirm_path", + "baseline": { + "oa": { + "description": "", + "name": "Continue" + }, + "sa": { + "description": "", + "name": "Continue" + } + }, + "description": "", + "id": "a106d183-c239-5c9f-b1f1-141af19fc778", + "kind": "Manual", + "name": "Continue ↔ Continue", + "oa": "229ee94f-d147-50f6-9769-b5bd0c03a084", + "sa": "1e738f27-a3a1-526d-b330-6384088b9689" + }, + "a5c1eb01-a95f-5f7a-9742-7f201493d7aa": { + "alias": "trace_oa_assess_then_decide", + "baseline": { + "oa": { + "description": "", + "name": "Continue" + }, + "sa": { + "description": "", + "name": "Continue" + } + }, + "description": "", + "id": "a5c1eb01-a95f-5f7a-9742-7f201493d7aa", + "kind": "Manual", + "name": "Continue ↔ Continue", + "oa": "109e2d6b-7e86-5934-93a5-fab56eced84e", + "sa": "9d4a131f-36ad-5125-b21f-c3dd033b8c82" + }, + "aa164208-394d-5bc7-8462-b0043f682aad": { + "alias": "trace_oa_mission_delivery", + "baseline": { + "oa": { + "description": "", + "name": "Mission capability: Restore a safe supply" + }, + "sa": { + "description": "", + "name": "Mission capability: Restore a safe supply" + } + }, + "description": "", + "id": "aa164208-394d-5bc7-8462-b0043f682aad", + "kind": "Manual", + "name": "Mission capability: Restore a safe supply ↔ Mission capability: Restore a safe supply", + "oa": "1f8b1898-65f6-5360-99cf-b78a71bd50ec", + "sa": "a0902e0f-3d18-576a-9aca-975a136dfdd7" + }, + "aba528ea-e396-582a-bfba-f6e2212d391a": { + "alias": "trace_oa_control", + "baseline": { + "oa": { + "description": "", + "name": "Control room" + }, + "sa": { + "description": "", + "name": "Pressure coordination service" + } + }, + "description": "", + "id": "aba528ea-e396-582a-bfba-f6e2212d391a", + "kind": "Participant", + "name": "Control room ↔ Pressure coordination service", + "oa": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "sa": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" + }, + "ac3aa6b0-568e-5a3f-963a-172aa3cfc7a4": { + "alias": "trace_oa_repair_step", + "baseline": { + "oa": { + "description": "", + "name": "Repair the valve" + }, + "sa": { + "description": "", + "name": "Repair the valve" + } + }, + "description": "", + "id": "ac3aa6b0-568e-5a3f-963a-172aa3cfc7a4", + "kind": "Sequence", + "name": "Repair the valve ↔ Repair the valve", + "oa": "c59432fe-6092-5ee0-908d-9dbf39476471", + "sa": "0f195fea-3493-5ebd-966d-da9eddeb05d7" + }, + "acdbee9c-5017-5154-804e-50801f0376ec": { + "alias": "trace_oa_assess_allocation", + "baseline": { + "oa": { + "description": "", + "name": "Allocation: Assess pressure alarm" + }, + "sa": { + "description": "", + "name": "Allocation: Assess pressure alarm" + } + }, + "description": "", + "id": "acdbee9c-5017-5154-804e-50801f0376ec", + "kind": "Manual", + "name": "Allocation: Assess pressure alarm ↔ Allocation: Assess pressure alarm", + "oa": "594c61cd-94e0-5c07-890d-edfd520d7df9", + "sa": "9b9356d7-2db2-5a94-a47e-ea892cf02afe" + }, + "ad697c00-07d9-5799-89b3-2c78da8dff4d": { + "alias": "trace_oa_request_step", + "baseline": { + "oa": { + "description": "", + "name": "Request the field repair" + }, + "sa": { + "description": "", + "name": "Request the field repair" + } + }, + "description": "", + "id": "ad697c00-07d9-5799-89b3-2c78da8dff4d", + "kind": "Sequence", + "name": "Request the field repair ↔ Request the field repair", + "oa": "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "sa": "40d94322-baa2-5c81-b261-ff47cdc33782" + }, + "b13452bb-1a55-5ad6-b6d9-a37fb34e8d13": { + "alias": "trace_oa_repair", + "baseline": { + "oa": { + "description": "", + "name": "Repair valve in the field" + }, + "sa": { + "description": "", + "name": "Repair valve in the field" + } + }, + "description": "", + "id": "b13452bb-1a55-5ad6-b6d9-a37fb34e8d13", + "kind": "Refinement", + "name": "Repair valve in the field ↔ Repair valve in the field", + "oa": "a21465af-05f3-5fb5-8983-de26932422fb", + "sa": "9497e10f-04c4-5586-8738-2464a6f00bd4" + }, + "b53863a2-ed80-5f98-ad3c-50e1b821066d": { + "alias": "trace_oa_monitor", + "baseline": { + "oa": { + "description": "", + "name": "Pressure supervision" + }, + "sa": { + "description": "", + "name": "Pressure supervision" + } + }, + "description": "", + "id": "b53863a2-ed80-5f98-ad3c-50e1b821066d", + "kind": "State", + "name": "Pressure supervision ↔ Pressure supervision", + "oa": "1970608f-85d1-5234-ac86-5e2b0df27dc3", + "sa": "132b3f28-3f88-5327-9868-8c8b645ed412" + }, + "b97749d4-f9ad-5fb7-9d78-bc60a74e98eb": { + "alias": "trace_oa_high_pressure", + "baseline": { + "oa": { + "description": "", + "name": "Pressure alarm detected" + }, + "sa": { + "description": "", + "name": "Pressure alarm detected" + } + }, + "description": "", + "id": "b97749d4-f9ad-5fb7-9d78-bc60a74e98eb", + "kind": "State", + "name": "Pressure alarm detected ↔ Pressure alarm detected", + "oa": "65d6b8d6-271a-5c84-8b42-abbe310c955e", + "sa": "01490fa8-2f01-5ac8-9722-d55d45414379" + }, + "bb72b2c2-6e32-5f65-9d8e-b33c1a65da37": { + "alias": "trace_oa_field_procedure", + "baseline": { + "oa": { + "description": "", + "name": "Field repair procedure" + }, + "sa": { + "description": "", + "name": "Field repair procedure" + } + }, + "description": "", + "id": "bb72b2c2-6e32-5f65-9d8e-b33c1a65da37", + "kind": "Sequence", + "name": "Field repair procedure ↔ Field repair procedure", + "oa": "5d3af417-244c-54bb-9588-16daedda4e1f", + "sa": "93eb6479-54e9-59cb-ac7b-16912347bfc0" + }, + "bbf6f1a1-cb2b-5632-8ec0-bf4b8ef1a941": { + "alias": "trace_oa_decision", + "baseline": { + "oa": { + "description": "", + "name": "Can a remote reset safely recover pressure?" + }, + "sa": { + "description": "", + "name": "Can a remote reset safely recover pressure?" + } + }, + "description": "", + "id": "bbf6f1a1-cb2b-5632-8ec0-bf4b8ef1a941", + "kind": "Sequence", + "name": "Can a remote reset safely recover pressure? ↔ Can a remote reset safely recover pressure?", + "oa": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "sa": "30c8fad9-a49b-5692-9542-3148e76afd7b" + }, + "c132a0df-1270-55a8-ac4e-6e5075823dc1": { + "alias": "trace_oa_outer_technician", + "baseline": { + "oa": { + "description": "", + "name": "Technician" + }, + "sa": { + "description": "", + "name": "Technician" + } + }, + "description": "", + "id": "c132a0df-1270-55a8-ac4e-6e5075823dc1", + "kind": "Scenario", + "name": "Technician ↔ Technician", + "oa": "fffff882-78fd-5415-928d-c1ef7fac6576", + "sa": "155e4fe4-9e3b-5f4e-b886-28abcc3f086b" + }, + "c47cf0b4-fe15-5175-bd62-5292e43adc39": { + "alias": "trace_oa_normal_transition", + "baseline": { + "oa": { + "description": "", + "name": "State transition: Monitoring" + }, + "sa": { + "description": "", + "name": "State transition: Monitoring" + } + }, + "description": "", + "id": "c47cf0b4-fe15-5175-bd62-5292e43adc39", + "kind": "Manual", + "name": "State transition: Monitoring ↔ State transition: Monitoring", + "oa": "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97", + "sa": "6e2c1b47-764c-577a-b325-ff66dd2a2768" + }, + "c5051628-b7af-5fb1-a014-bb60a0b46e03": { + "alias": "trace_oa_responder", + "baseline": { + "oa": { + "description": "", + "name": "Technician" + }, + "sa": { + "description": "", + "name": "Technician" + } + }, + "description": "", + "id": "c5051628-b7af-5fb1-a014-bb60a0b46e03", + "kind": "Scenario", + "name": "Technician ↔ Technician", + "oa": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "sa": "6249b54a-e396-5338-82b9-89266c55670d" + }, + "c987bd0f-87f6-5550-945d-2a42450fe1c5": { + "alias": "trace_oa_mission", + "baseline": { + "oa": { + "description": "", + "name": "Maintain safe water supply" + }, + "sa": { + "description": "", + "name": "Maintain safe water supply" + } + }, + "description": "", + "id": "c987bd0f-87f6-5550-945d-2a42450fe1c5", + "kind": "Mission", + "name": "Maintain safe water supply ↔ Maintain safe water supply", + "oa": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90", + "sa": "5ad034b1-084d-5273-9e70-971671766132" + }, + "c9d65349-3400-58eb-8a55-57fa02de4b67": { + "alias": "trace_oa_contact", + "baseline": { + "oa": { + "description": "", + "name": "Technician service conversation" + }, + "sa": { + "description": "", + "name": "Technician service conversation" + } + }, + "description": "", + "id": "c9d65349-3400-58eb-8a55-57fa02de4b67", + "kind": "Exchange", + "name": "Technician service conversation ↔ Technician service conversation", + "oa": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "sa": "63009b5f-539e-5914-b630-ec6b7fefb4d5" + }, + "cf92cedf-ea1a-510c-9bc5-89c16d60e8d2": { + "alias": "trace_oa_repair_operation", + "baseline": { + "oa": { + "description": "", + "name": "Valve repair" + }, + "sa": { + "description": "", + "name": "Valve repair" + } + }, + "description": "", + "id": "cf92cedf-ea1a-510c-9bc5-89c16d60e8d2", + "kind": "Item", + "name": "Valve repair ↔ Valve repair", + "oa": "d736c7d6-d369-550a-8ba4-dae223e40bb3", + "sa": "a962fc5e-3ab0-5296-8b0d-fd84633f9218" + }, + "d32dd433-9c9e-5b57-9355-2970cc44a5f2": { + "alias": "trace_oa_field_merge", + "baseline": { + "oa": { + "description": "", + "name": "Continue" + }, + "sa": { + "description": "", + "name": "Continue" + } + }, + "description": "", + "id": "d32dd433-9c9e-5b57-9355-2970cc44a5f2", + "kind": "Manual", + "name": "Continue ↔ Continue", + "oa": "c8593670-8906-530f-b6a2-3ada77a9c219", + "sa": "0f6ac3fb-4ad6-581e-ad14-336472072d5e" + }, + "d3626818-4846-5893-9c02-3407cdba7f3c": { + "alias": "trace_oa_response", + "baseline": { + "oa": { + "description": "", + "name": "Handle a pressure alarm" + }, + "sa": { + "description": "", + "name": "Handle a pressure alarm" + } + }, + "description": "", + "id": "d3626818-4846-5893-9c02-3407cdba7f3c", + "kind": "Sequence", + "name": "Handle a pressure alarm ↔ Handle a pressure alarm", + "oa": "58bae2b0-f560-50f2-818d-19091d22d18a", + "sa": "5139ce73-56fd-5875-ba3a-e569fc890231" + }, + "d47d3c36-50fb-5189-b801-d6fd1694bd49": { + "alias": "trace_oa_technician", + "baseline": { + "oa": { + "description": "", + "name": "Field technician" + }, + "sa": { + "description": "", + "name": "Field technician" + } + }, + "description": "", + "id": "d47d3c36-50fb-5189-b801-d6fd1694bd49", + "kind": "Participant", + "name": "Field technician ↔ Field technician", + "oa": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "sa": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a" + }, + "d7773ff5-cef5-50c1-8f9d-c5659115e1f0": { + "alias": "trace_oa_confirm", + "baseline": { + "oa": { + "description": "", + "name": "Confirm safe pressure" + }, + "sa": { + "description": "", + "name": "Confirm safe pressure" + } + }, + "description": "", + "id": "d7773ff5-cef5-50c1-8f9d-c5659115e1f0", + "kind": "Refinement", + "name": "Confirm safe pressure ↔ Confirm safe pressure", + "oa": "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "sa": "d75eb3fa-a27d-52d5-8782-8b342b3865f6" + }, + "d9d47371-e98d-5d29-9df7-4de2d28a5c73": { + "alias": "trace_oa_pressure", + "baseline": { + "oa": { + "description": "", + "name": "Measured pressure" + }, + "sa": { + "description": "", + "name": "Measured pressure" + } + }, + "description": "", + "id": "d9d47371-e98d-5d29-9df7-4de2d28a5c73", + "kind": "Item", + "name": "Measured pressure ↔ Measured pressure", + "oa": "e7824764-5db9-5384-8198-2fbaea2b10b2", + "sa": "87d0a95b-af1b-5352-b753-e4c6533671a5" + }, + "da8efc3c-78df-5b10-b2e5-1e28d6d99e86": { + "alias": "trace_oa_repair_request", + "baseline": { + "oa": { + "description": "", + "name": "Request field repair" + }, + "sa": { + "description": "", + "name": "Request field repair" + } + }, + "description": "", + "id": "da8efc3c-78df-5b10-b2e5-1e28d6d99e86", + "kind": "Exchange", + "name": "Request field repair ↔ Request field repair", + "oa": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "sa": "93474111-f23e-56c1-b61b-b7c2396b91bd" + }, + "e63b14d2-f408-56a2-aec4-be20cf4ace73": { + "alias": "trace_oa_report_step", + "baseline": { + "oa": { + "description": "", + "name": "Report the measured result" + }, + "sa": { + "description": "", + "name": "Report the measured result" + } + }, + "description": "", + "id": "e63b14d2-f408-56a2-aec4-be20cf4ace73", + "kind": "Sequence", + "name": "Report the measured result ↔ Report the measured result", + "oa": "df2fd3f8-6bc2-5380-a13f-36643ed35ac7", + "sa": "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac" + }, + "e848da41-445e-5e3c-8b4e-ef53fdddf1e1": { + "alias": "trace_oa_assess_step", + "baseline": { + "oa": { + "description": "", + "name": "Assess the alarm" + }, + "sa": { + "description": "", + "name": "Assess the alarm" + } + }, + "description": "", + "id": "e848da41-445e-5e3c-8b4e-ef53fdddf1e1", + "kind": "Sequence", + "name": "Assess the alarm ↔ Assess the alarm", + "oa": "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "sa": "9024243c-ccc1-5edb-8477-2aa16e5e9a50" + }, + "e96ff119-bb4d-5fd9-9ab7-2ede0d3fd418": { + "alias": "trace_oa_contact_category", + "baseline": { + "oa": { + "description": "", + "name": "Field contact" + }, + "sa": { + "description": "", + "name": "Field contact" + } + }, + "description": "", + "id": "e96ff119-bb4d-5fd9-9ab7-2ede0d3fd418", + "kind": "Manual", + "name": "Field contact ↔ Field contact", + "oa": "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771", + "sa": "781cbe75-410f-5cc5-aeb8-a43491f77b99" + }, + "eee79ec1-41d7-5374-986e-995a56b9026f": { + "alias": "trace_oa_remote_path", + "baseline": { + "oa": { + "description": "", + "name": "remote reset is authorized" + }, + "sa": { + "description": "", + "name": "remote reset is authorized" + } + }, + "description": "", + "id": "eee79ec1-41d7-5374-986e-995a56b9026f", + "kind": "Manual", + "name": "remote reset is authorized ↔ remote reset is authorized", + "oa": "76218d89-68fd-531d-b35b-90c5b769a5ef", + "sa": "d8b6710a-e713-5979-934f-e5ca7d155bb4" + }, + "f0cc86e5-f8fd-5091-a40d-dbfb6515072f": { + "alias": "trace_oa_call", + "baseline": { + "oa": { + "description": "", + "name": "Request valve repair" + }, + "sa": { + "description": "", + "name": "Request valve repair" + } + }, + "description": "", + "id": "f0cc86e5-f8fd-5091-a40d-dbfb6515072f", + "kind": "Scenario", + "name": "Request valve repair ↔ Request valve repair", + "oa": "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "sa": "8bab4783-a098-5984-9e36-493957caf26c" + }, + "f2b7050c-629d-533f-b873-bffc33e072f7": { + "alias": "trace_oa_request_then_repair", + "baseline": { + "oa": { + "description": "", + "name": "Sequence link: Repair the valve" + }, + "sa": { + "description": "", + "name": "Sequence link: Repair the valve" + } + }, + "description": "", + "id": "f2b7050c-629d-533f-b873-bffc33e072f7", + "kind": "Manual", + "name": "Sequence link: Repair the valve ↔ Sequence link: Repair the valve", + "oa": "3f54d890-4ca5-5e36-873b-abb5c92d578c", + "sa": "6a6d968f-26d6-5dbb-b721-5a27d362779a" + }, + "f5733d3f-eee3-5cf7-a422-5edd2054d6a6": { + "alias": "trace_oa_use_conversation", + "baseline": { + "oa": { + "description": "", + "name": "Use the reviewed repair conversation" + }, + "sa": { + "description": "", + "name": "Use the reviewed repair conversation" + } + }, + "description": "", + "id": "f5733d3f-eee3-5cf7-a422-5edd2054d6a6", + "kind": "Scenario", + "name": "Use the reviewed repair conversation ↔ Use the reviewed repair conversation", + "oa": "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5", + "sa": "1c9da843-e2d3-5591-8e3f-db083a42867e" + }, + "f8d34715-1dc8-504b-acdf-78b770aad1e8": { + "alias": "trace_oa_repair_then_report", + "baseline": { + "oa": { + "description": "", + "name": "Sequence link: Report the measured result" + }, + "sa": { + "description": "", + "name": "Sequence link: Report the measured result" + } + }, + "description": "", + "id": "f8d34715-1dc8-504b-acdf-78b770aad1e8", + "kind": "Manual", + "name": "Sequence link: Report the measured result ↔ Sequence link: Report the measured result", + "oa": "c8196d35-2168-5fef-8b03-ba0d139bc00f", + "sa": "7fb30b1d-195c-5f0e-a19c-a536f519a899" + }, + "fb02420d-b066-5773-87c1-198a056e0731": { + "alias": "trace_oa_assess_on_entry", + "baseline": { + "oa": { + "description": "", + "name": "Assess when repair is needed" + }, + "sa": { + "description": "", + "name": "Assess when repair is needed" + } + }, + "description": "", + "id": "fb02420d-b066-5773-87c1-198a056e0731", + "kind": "Manual", + "name": "Assess when repair is needed ↔ Assess when repair is needed", + "oa": "7fa37e63-db65-5aa7-88f2-31ff4347a787", + "sa": "a887f96a-37cb-5891-8070-011fa276e5b5" + }, + "fd195498-dbba-523b-94d0-49f3b9531a3e": { + "alias": "trace_oa_outer_controller", + "baseline": { + "oa": { + "description": "", + "name": "Controller" + }, + "sa": { + "description": "", + "name": "Controller" + } + }, + "description": "", + "id": "fd195498-dbba-523b-94d0-49f3b9531a3e", + "kind": "Scenario", + "name": "Controller ↔ Controller", + "oa": "2011b3ac-614b-5a47-8891-7ff81965f425", + "sa": "1edfcf9e-9bfa-51f1-80d5-62b8fd8bb023" + }, + "ff84fc06-6f99-5cf3-b73d-236e9eff98a3": { + "alias": "trace_oa_reset_step", + "baseline": { + "oa": { + "description": "", + "name": "Try the authorized remote reset" + }, + "sa": { + "description": "", + "name": "Try the authorized remote reset" + } + }, + "description": "", + "id": "ff84fc06-6f99-5cf3-b73d-236e9eff98a3", + "kind": "Sequence", + "name": "Try the authorized remote reset ↔ Try the authorized remote reset", + "oa": "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "sa": "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6" + } + } +} diff --git a/examples/release1/model.rs b/examples/release1/model.rs new file mode 100644 index 00000000..dc605c6f --- /dev/null +++ b/examples/release1/model.rs @@ -0,0 +1,1037 @@ +//! Reproducible teaching model. The SA counterpart is produced by the real planner. +use oa_core::{source_project, transitions::*, validation, view_options::*, *}; +use std::collections::BTreeMap; + +pub fn id(alias: &str) -> Id { + Id::new_v5( + &Id::NAMESPACE_URL, + format!("https://arcadia-studio.example/lessons/pressure/{alias}").as_bytes(), + ) +} +fn element( + p: &mut Project, + alias: &str, + name: &str, + kind: Kind, + layer: Layer, + parent: Option, +) -> Id { + let id = id(alias); + p.elements.insert( + id, + Element { + id, + alias: alias.into(), + name: name.into(), + description: String::new(), + parent, + body: ElementBody::new(kind, layer).unwrap(), + }, + ); + id +} +fn shared(p: &mut Project, id: Id, body: SharedBody) { + *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body; +} +fn relation(p: &mut Project, alias: &str, name: &str, body: RelationBody) -> Id { + let id = id(alias); + p.relations.insert( + id, + Relation { + id, + alias: alias.into(), + name: name.into(), + description: String::new(), + body, + }, + ); + id +} +fn link(p: &mut Project, alias: &str, kind: RelationKind, source: Id, target: Id) -> Id { + let name = format!("{kind}: {}", p.named(target)); + relation(p, alias, &name, RelationBody::new(kind, source, target)) +} +fn occurrence(p: &mut Project, owner: Id, alias: &str, name: &str, body: OccurrenceBody) -> Id { + let layer = p.elements[&owner].layer(); + let id = element(p, alias, name, Kind::Occurrence, layer, Some(owner)); + shared( + p, + id, + SharedBody::Occurrence { + body, + precondition: String::new(), + postcondition: String::new(), + }, + ); + p.elements + .get_mut(&owner) + .unwrap() + .steps_mut() + .unwrap() + .push(id); + id +} +fn line(p: &mut Project, owner: Id, alias: &str, name: &str, represented: Id) -> Id { + let id = element(p, alias, name, Kind::Lifeline, Layer::Oa, Some(owner)); + shared( + p, + id, + SharedBody::Lifeline { + represented: Some(represented), + }, + ); + id +} +fn scenario(p: &mut Project, alias: &str, name: &str, capability: Id) -> Id { + let id = element(p, alias, name, Kind::Scenario, Layer::Oa, None); + shared( + p, + id, + SharedBody::Scenario { + viewpoint: ScenarioViewpoint::ParticipantInteraction, + context: Some(capability), + precondition: "A pressure alarm has been assessed".into(), + postcondition: "The controller has received the measured pressure after repair".into(), + steps: vec![], + }, + ); + id +} +fn message( + exchange: Id, + sender: Id, + receiver: Id, + reply_to: Option, + field: Id, + value: &str, +) -> OccurrenceBody { + OccurrenceBody::Exchange { + exchange: Some(exchange), + sender: Some(sender), + receiver: Some(receiver), + message_kind: if reply_to.is_some() { + MessageKind::Reply + } else { + MessageKind::Call + }, + reply_to, + values: vec![MessageValue { + path: vec![], + field, + expression: value.into(), + }], + } +} +fn diagram( + p: &mut Project, + alias: &str, + name: &str, + layer: Layer, + projection: ProjectionKind, + members: &[(&str, f32, f32)], +) -> Id { + let diagram_id = id(alias); + let nodes = members + .iter() + .map(|(member, x, y)| { + let alias = format!("{alias}_{member}"); + let node = NodePlacement { + id: id(&alias), + alias, + element: id(member), + position: Position { x: *x, y: *y }, + size: Size { + width: 245., + height: 110., + }, + collapsed: false, + port_anchors: vec![], + }; + (node.id, node) + }) + .collect(); + p.diagrams.insert(diagram_id, Diagram { id: diagram_id, alias: alias.into(), name: name.into(), description: "Teaching view: positions and filters do not change ownership or exchange identities.".into(), layer, projection, view_options: ViewOptions::default(), nodes, routes: vec![] }); + diagram_id +} + +pub struct Lesson { + pub project: Project, + pub before_transition: Project, + pub request: TransitionRequest, +} + +pub fn build() -> Lesson { + let mut p = Project::empty(); + p.id = id("pressure_restoration"); + p.alias = "pressure_restoration".into(); + p.name = "Restore safe water pressure — release 1 walkthrough".into(); + p.description = "A controller either resets a valve remotely or requests field repair. Compare the operational need with the explicitly chosen coordination-service boundary. Start with README.md beside project.arcadia. Units and guards are descriptive; this model does not simulate a real water network.".into(); + let control = element( + &mut p, + "oa_control", + "Control room", + Kind::Entity, + Layer::Oa, + None, + ); + let technician = element( + &mut p, + "oa_technician", + "Field technician", + Kind::Actor, + Layer::Oa, + None, + ); + let parent = element( + &mut p, + "oa_restore_work", + "Restore safe pressure", + Kind::Activity, + Layer::Oa, + None, + ); + let mut works = vec![]; + for (alias, name, performer) in [ + ("oa_assess", "Assess pressure alarm", control), + ("oa_reset", "Reset valve remotely", control), + ("oa_repair", "Repair valve in the field", technician), + ("oa_confirm", "Confirm safe pressure", control), + ] { + let work = element(&mut p, alias, name, Kind::Activity, Layer::Oa, Some(parent)); + link( + &mut p, + &format!("{alias}_allocation"), + RelationKind::Allocation, + performer, + work, + ); + works.push(work); + } + let [assess, reset, repair, confirm] = works[..] else { + unreachable!() + }; + let mission = element( + &mut p, + "oa_mission", + "Maintain safe water supply", + Kind::OperationalMission, + Layer::Oa, + None, + ); + let capability = element( + &mut p, + "oa_restore", + "Restore a safe supply", + Kind::Capability, + Layer::Oa, + None, + ); + let triage = element( + &mut p, + "oa_triage", + "Assess an alarm", + Kind::Capability, + Layer::Oa, + None, + ); + link( + &mut p, + "oa_mission_delivery", + RelationKind::MissionCapability, + mission, + capability, + ); + link( + &mut p, + "oa_mission_participant", + RelationKind::MissionInvolvement, + control, + mission, + ); + link( + &mut p, + "oa_include_triage", + RelationKind::CapabilityInclude, + capability, + triage, + ); + link( + &mut p, + "oa_assessment_support", + RelationKind::CapabilityInvolvement, + assess, + triage, + ); + + let pressure = element( + &mut p, + "oa_pressure", + "Measured pressure", + Kind::DataType, + Layer::Oa, + None, + ); + shared( + &mut p, + pressure, + SharedBody::DataType { + definition: DataTypeDefinition::Real { + minimum: Some(0.), + maximum: Some(600.), + }, + default: None, + unit: "kPa".into(), + }, + ); + let ticket = element( + &mut p, + "oa_repair_operation", + "Valve repair", + Kind::ExchangeItem, + Layer::Oa, + None, + ); + shared( + &mut p, + ticket, + SharedBody::ExchangeItem { + kind: ExchangeItemKind::Information, + mechanism: ExchangeMechanism::Operation, + supertype: None, + }, + ); + let measured = element( + &mut p, + "oa_pressure_parameter", + "Pressure before / after repair", + Kind::DataField, + Layer::Oa, + Some(ticket), + ); + shared( + &mut p, + measured, + SharedBody::DataField { + type_ref: TypeRef::Named(pressure), + minimum: 1, + maximum: Some(1), + unit: String::new(), + direction: ParameterDirection::InOut, + storage: FieldStorage::Value, + default: None, + }, + ); + let request_exchange = relation( + &mut p, + "oa_repair_request", + "Request field repair", + RelationBody::Interaction { + source: assess, + target: repair, + items: vec![ticket], + }, + ); + let report_exchange = relation( + &mut p, + "oa_repair_report", + "Report repair result", + RelationBody::Interaction { + source: repair, + target: confirm, + items: vec![ticket], + }, + ); + let channel = relation( + &mut p, + "oa_contact", + "Technician service conversation", + RelationBody::CommunicationMean { + source: control, + target: technician, + oriented: false, + items: vec![ticket], + }, + ); + for (alias, exchange) in [ + ("oa_request_carriage", request_exchange), + ("oa_report_carriage", report_exchange), + ] { + link( + &mut p, + alias, + RelationKind::ExchangeAllocation, + channel, + exchange, + ); + } + for (alias, name, family, exchanges) in [ + ( + "oa_repair_traffic", + "Repair work exchanges", + ExchangeFamily::Work, + vec![request_exchange, report_exchange], + ), + ( + "oa_contact_category", + "Field contact", + ExchangeFamily::Participant, + vec![channel], + ), + ] { + let category = element(&mut p, alias, name, Kind::ExchangeCategory, Layer::Oa, None); + shared( + &mut p, + category, + SharedBody::ExchangeCategory { family, exchanges }, + ); + } + + let assessment = element( + &mut p, + "oa_assessment_procedure", + "Assess an alarm procedure", + Kind::Process, + Layer::Oa, + None, + ); + occurrence( + &mut p, + assessment, + "oa_assessment_action", + "Assess pressure alarm", + OccurrenceBody::Action { + action: Some(assess), + lifeline: None, + }, + ); + link( + &mut p, + "oa_assessment_delivery", + RelationKind::ChainInvolvement, + assessment, + triage, + ); + let field = element( + &mut p, + "oa_field_procedure", + "Field repair procedure", + Kind::Process, + Layer::Oa, + None, + ); + let ask = occurrence( + &mut p, + field, + "oa_request_step", + "Request the field repair", + OccurrenceBody::Exchange { + exchange: Some(request_exchange), + sender: None, + receiver: None, + message_kind: MessageKind::Asynchronous, + reply_to: None, + values: vec![], + }, + ); + let fix = occurrence( + &mut p, + field, + "oa_repair_step", + "Repair the valve", + OccurrenceBody::Action { + action: Some(repair), + lifeline: None, + }, + ); + let report = occurrence( + &mut p, + field, + "oa_report_step", + "Report the measured result", + OccurrenceBody::Exchange { + exchange: Some(report_exchange), + sender: None, + receiver: None, + message_kind: MessageKind::Asynchronous, + reply_to: None, + values: vec![], + }, + ); + link( + &mut p, + "oa_request_then_repair", + RelationKind::SequenceLink, + ask, + fix, + ); + link( + &mut p, + "oa_repair_then_report", + RelationKind::SequenceLink, + fix, + report, + ); + let process = element( + &mut p, + "oa_response", + "Handle a pressure alarm", + Kind::Process, + Layer::Oa, + None, + ); + link( + &mut p, + "oa_response_delivery", + RelationKind::ChainInvolvement, + process, + capability, + ); + let start = occurrence( + &mut p, + process, + "oa_assess_step", + "Assess the alarm", + OccurrenceBody::SequenceReference { + sequence: Some(assessment), + }, + ); + let choice = occurrence( + &mut p, + process, + "oa_decision", + "Can a remote reset safely recover pressure?", + OccurrenceBody::Control { + kind: ControlKind::Choice, + }, + ); + let remote = occurrence( + &mut p, + process, + "oa_reset_step", + "Try the authorized remote reset", + OccurrenceBody::Action { + action: Some(reset), + lifeline: None, + }, + ); + let reuse = occurrence( + &mut p, + process, + "oa_use_field", + "Use the field repair procedure", + OccurrenceBody::SequenceReference { + sequence: Some(field), + }, + ); + let merge = occurrence( + &mut p, + process, + "oa_merge", + "Recovery path completed", + OccurrenceBody::Control { + kind: ControlKind::Merge, + }, + ); + let end = occurrence( + &mut p, + process, + "oa_confirm_step", + "Confirm safe pressure", + OccurrenceBody::Action { + action: Some(confirm), + lifeline: None, + }, + ); + for (alias, a, b, guard) in [ + ("oa_assess_then_decide", start, choice, ""), + ( + "oa_remote_path", + choice, + remote, + "remote reset is authorized", + ), + ("oa_field_path", choice, reuse, "else"), + ("oa_remote_merge", remote, merge, ""), + ("oa_field_merge", reuse, merge, ""), + ("oa_confirm_path", merge, end, ""), + ] { + relation( + &mut p, + alias, + if guard.is_empty() { "Continue" } else { guard }, + RelationBody::SequenceLink { + source: a, + target: b, + guard: guard.into(), + }, + ); + } + let conversation = scenario( + &mut p, + "oa_conversation", + "Repair request and measured reply", + capability, + ); + let caller = line(&mut p, conversation, "oa_caller", "Controller", control); + let responder = line( + &mut p, + conversation, + "oa_responder", + "Technician", + technician, + ); + let call = occurrence( + &mut p, + conversation, + "oa_call", + "Request valve repair", + message(channel, caller, responder, None, measured, "420.0"), + ); + occurrence( + &mut p, + conversation, + "oa_reply", + "Return pressure after repair", + message(channel, responder, caller, Some(call), measured, "320.0"), + ); + let full = scenario( + &mut p, + "oa_full_scenario", + "Restore supply through field repair", + capability, + ); + let outer_control = line(&mut p, full, "oa_outer_controller", "Controller", control); + let outer_tech = line( + &mut p, + full, + "oa_outer_technician", + "Technician", + technician, + ); + occurrence( + &mut p, + full, + "oa_use_conversation", + "Use the reviewed repair conversation", + OccurrenceBody::ScenarioUse { + scenario: Some(conversation), + bindings: vec![ + LifelineBinding { + referenced: caller, + local: outer_control, + }, + LifelineBinding { + referenced: responder, + local: outer_tech, + }, + ], + }, + ); + + let machine = element( + &mut p, + "oa_monitor", + "Pressure supervision", + Kind::StateMachine, + Layer::Oa, + Some(control), + ); + let region = element( + &mut p, + "oa_monitor_region", + "Supervision cycle", + Kind::Region, + Layer::Oa, + Some(machine), + ); + let initial = element( + &mut p, + "oa_monitor_initial", + "Start", + Kind::Initial, + Layer::Oa, + Some(region), + ); + let watch = element( + &mut p, + "oa_monitoring", + "Monitoring", + Kind::State, + Layer::Oa, + Some(region), + ); + let waiting = element( + &mut p, + "oa_waiting", + "Awaiting repair", + Kind::State, + Layer::Oa, + Some(region), + ); + let high = element( + &mut p, + "oa_high_pressure", + "Pressure alarm detected", + Kind::Event, + Layer::Oa, + None, + ); + shared( + &mut p, + high, + SharedBody::Event { + body: EventBody::Change { + expression: "measured_pressure > operating_limit".into(), + }, + }, + ); + link( + &mut p, + "oa_start_monitoring", + RelationKind::StateTransition, + initial, + watch, + ); + let alarm = link( + &mut p, + "oa_alarm_transition", + RelationKind::StateTransition, + watch, + waiting, + ); + if let RelationBody::StateTransition { triggers, .. } = + &mut p.relations.get_mut(&alarm).unwrap().body + { + triggers.push(high); + } + let normal = link( + &mut p, + "oa_normal_transition", + RelationKind::StateTransition, + waiting, + watch, + ); + if let RelationBody::StateTransition { guard, .. } = + &mut p.relations.get_mut(&normal).unwrap().body + { + *guard = "repair confirmed and pressure is safe".into(); + } + relation( + &mut p, + "oa_assess_on_entry", + "Assess when repair is needed", + RelationBody::StateAction { + source: waiting, + target: assess, + phase: StateActionKind::Entry, + }, + ); + + // The engineer chooses this system boundary; the planner must never infer it. + let system = element( + &mut p, + "sa_control", + "Pressure coordination service", + Kind::System, + Layer::Sa, + None, + ); + let external = element( + &mut p, + "sa_technician", + "Field technician", + Kind::SystemActor, + Layer::Sa, + None, + ); + let mut request = TransitionRequest::new( + TransitionDirection::OaToSa, + p.elements + .values() + .filter(|e| e.layer() == Layer::Oa) + .map(|e| e.id) + .chain( + p.relations + .values() + .filter(|r| r.kind() != RelationKind::ExchangeAllocation) + .map(|r| r.id), + ) + .collect(), + ); + request.fields.clear(); // Existing selected participants retain their deliberately different names. + request.mappings = vec![ + TransitionMapping { + source: control, + target: Some(system), + target_kind: None, + }, + TransitionMapping { + source: technician, + target: Some(external), + target_kind: None, + }, + ]; + let port = |alias: &str, name: &str| CommunicationPortChoice::Create { + id: id(alias), + alias: alias.into(), + name: name.into(), + description: "Explicitly chosen Standard component port for the repair service.".into(), + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }; + request + .communication_endpoints + .push(CommunicationEndpointDecision { + source: channel, + source_port: port("sa_control_port", "Repair service client"), + target_port: port("sa_technician_port", "Repair service provider"), + }); + let before_transition = p.clone(); + let plan = plan_transition(&p, &request).expect("well-formed teaching transition"); + assert!( + plan.conflicts.is_empty(), + "teaching transition conflicts: {:?}", + plan.conflicts + ); + p = apply_transition(&p, &plan).expect("teaching transition is structurally valid"); + + // Canonical lesson aliases/UUIDs replace only generator-created identities. + // The real planner still determines every counterpart and function-port contract. + for change in &plan.changes { + let alias = before_transition + .identities() + .into_iter() + .find(|(id, _)| *id == change.source) + .unwrap() + .1 + .replacen("oa_", "sa_", 1); + if let Some(e) = p.elements.get_mut(&change.target) { + e.alias = alias.clone(); + } + if let Some(r) = p.relations.get_mut(&change.target) { + r.alias = alias; + } + } + let functions: Vec<_> = p + .relations + .values() + .filter_map(|r| { + if let RelationBody::FunctionalExchange { + source_port, + target_port, + .. + } = r.body + { + Some((r.alias.clone(), source_port, target_port)) + } else { + None + } + }) + .collect(); + for (alias, out, input) in functions { + p.elements.get_mut(&out).unwrap().alias = format!("{alias}_out"); + p.elements.get_mut(&input).unwrap().alias = format!("{alias}_in"); + } + let aliases: BTreeMap<_, _> = p + .identities() + .into_iter() + .map(|(id, alias)| (id, alias.to_owned())) + .collect(); + for trace in p.traces.values_mut() { + trace.alias = format!("trace_{}", aliases[&trace.oa]); + } + let identities: Vec<_> = p + .identities() + .into_iter() + .map(|(old, alias)| (old, id(alias))) + .collect(); + for (old, new) in identities { + session::reidentify(&mut p, old, new).unwrap(); + } + + let interface = element( + &mut p, + "sa_repair_interface", + "Valve repair service", + Kind::Interface, + Layer::Sa, + None, + ); + relation( + &mut p, + "sa_repair_interface_item", + "Repair operation contract", + RelationBody::InterfaceItemAllocation { + source: interface, + target: id("sa_repair_operation"), + order: 0, + transmission: TransmissionProtocol::Synchronous, + acquisition: AcquisitionProtocol::Accept, + }, + ); + link( + &mut p, + "sa_client_requires", + RelationKind::InterfaceRequirement, + id("sa_control_port"), + interface, + ); + link( + &mut p, + "sa_provider_provides", + RelationKind::InterfaceProvision, + id("sa_technician_port"), + interface, + ); + for (name, component, port) in [ + ("request_out", "sa_control_port", "sa_repair_request_out"), + ("request_in", "sa_technician_port", "sa_repair_request_in"), + ("report_out", "sa_technician_port", "sa_repair_report_out"), + ("report_in", "sa_control_port", "sa_repair_report_in"), + ] { + link( + &mut p, + &format!("sa_port_{name}"), + RelationKind::PortAllocation, + id(component), + id(port), + ); + } + for (alias, exchange) in [ + ("sa_request_carriage", "sa_repair_request"), + ("sa_report_carriage", "sa_repair_report"), + ] { + link( + &mut p, + alias, + RelationKind::ExchangeAllocation, + id("sa_contact"), + id(exchange), + ); + } + let audit = element( + &mut p, + "sa_audit_thresholds", + "Audit pressure thresholds", + Kind::Function, + Layer::Sa, + None, + ); + p.elements.get_mut(&audit).unwrap().description = "Deliberately unfinished: assigned to the service, but no behavior context yet. Model checks should expose this one work_without_behavior_context warning.".into(); + link( + &mut p, + "sa_audit_allocation", + RelationKind::Allocation, + id("sa_control"), + audit, + ); + let evidence = id("repair_test_evidence"); + p.artifact_links.insert( + evidence, + ArtifactLink { + id: evidence, + alias: "repair_test_evidence".into(), + name: "Repair conversation acceptance notes".into(), + description: "A local teaching artifact, not proof of a real valve's safety.".into(), + owner: id("sa_conversation"), + relation: ArtifactRelation::Verifies, + uri: "evidence/repair-check.md".into(), + revision: Some("lesson-1".into()), + fragment: Some("acceptance".into()), + }, + ); + + let layout = [ + ("control", 60., 80.), + ("technician", 850., 80.), + ("assess", 100., 170.), + ("reset", 100., 330.), + ("confirm", 100., 490.), + ("repair", 890., 170.), + ]; + for prefix in ["oa", "sa"] { + let names: Vec<_> = layout + .iter() + .map(|(n, x, y)| (format!("{prefix}_{n}"), *x, *y)) + .collect(); + let members: Vec<_> = names.iter().map(|(n, x, y)| (n.as_str(), *x, *y)).collect(); + diagram( + &mut p, + &format!("{prefix}_allocation_view"), + if prefix == "oa" { + "1 · OA responsibility and repair traffic" + } else { + "2 · SA service boundary and functions" + }, + if prefix == "oa" { Layer::Oa } else { Layer::Sa }, + ProjectionKind::Allocation, + &members, + ); + } + let mut focused = p.diagrams[&id("sa_allocation_view")].clone(); + focused.id = id("sa_service_view"); + focused.alias = "sa_service_view".into(); + focused.name = "3 · SA service conversation only".into(); + focused.nodes = focused + .nodes + .into_values() + .map(|mut n| { + n.alias = n.alias.replacen("sa_allocation_view", "sa_service_view", 1); + n.id = id(&n.alias); + (n.id, n) + }) + .collect(); + focused.view_options.work_categories = CategorySelection::Any(vec![]); + focused.view_options.participant_categories = + CategorySelection::Any(vec![id("sa_contact_category")]); + focused.view_options.exchange_labels = ExchangeLabels::NameAndItems; + p.diagrams.insert(focused.id, focused); + diagram( + &mut p, + "sa_capability_view", + "4 · SA mission and included capability", + Layer::Sa, + ProjectionKind::Capabilities, + &[ + ("sa_control", 60., 60.), + ("sa_mission", 420., 60.), + ("sa_restore", 420., 260.), + ("sa_triage", 800., 260.), + ], + ); + diagram( + &mut p, + "sa_state_view", + "5 · SA pressure supervision", + Layer::Sa, + ProjectionKind::StateMachine { + machine: id("sa_monitor"), + }, + &[ + ("sa_monitor", 30., 30.), + ("sa_monitor_region", 70., 95.), + ("sa_monitor_initial", 100., 200.), + ("sa_monitoring", 220., 200.), + ("sa_waiting", 650., 200.), + ], + ); + Lesson { + project: p, + before_transition, + request, + } +} + +pub fn checked_bundle(project: &Project) -> source_project::SourceBundle { + let issues = validation::validate(project); + let errors: Vec<_> = issues + .iter() + .filter(|i| i.severity == validation::Severity::Error) + .collect(); + assert!(errors.is_empty(), "teaching model errors: {errors:?}"); + let bundle = source_project::format_bundle(project); + let compiled = + source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap(); + assert_eq!(&compiled.decoded.project, project); + bundle +} diff --git a/examples/release1/oa/activities/activity_oa_restore_work.arcadia b/examples/release1/oa/activities/activity_oa_restore_work.arcadia new file mode 100644 index 00000000..78781701 --- /dev/null +++ b/examples/release1/oa/activities/activity_oa_restore_work.arcadia @@ -0,0 +1,45 @@ +oa_activity oa_restore_work "Restore safe pressure" id "1b91f8a7-0bdb-514e-b1cd-955658e09927" { + description ""; + parent none; +} + +oa_activity oa_assess "Assess pressure alarm" id "61f7bbfb-74a4-5540-9182-2583dfb27a49" { + description ""; + parent @oa_restore_work; +} + +oa_activity oa_confirm "Confirm safe pressure" id "727c0b5c-8db6-5eb7-83d6-735519c5949f" { + description ""; + parent @oa_restore_work; +} + +oa_activity oa_repair "Repair valve in the field" id "a21465af-05f3-5fb5-8983-de26932422fb" { + description ""; + parent @oa_restore_work; +} + +oa_activity oa_reset "Reset valve remotely" id "d0c8be87-d238-503a-a549-7558a210507f" { + description ""; + parent @oa_restore_work; +} + +capability_involvement oa_assessment_support "Capability involvement: Assess an alarm" id "ffdbfcd0-d423-5707-a880-0dfbe1400f72" { + description ""; + source @oa_assess; + target @oa_triage; +} + +interaction oa_repair_request "Request field repair" id "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d" { + description ""; + items [@oa_repair_operation]; + source @oa_assess; + target @oa_repair; +} + +interaction oa_repair_report "Report repair result" id "b436d0b3-c824-5a96-af6b-2c269816d1cc" { + description ""; + items [@oa_repair_operation]; + source @oa_repair; + target @oa_confirm; +} + diff --git a/examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia b/examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia new file mode 100644 index 00000000..4ab5bda8 --- /dev/null +++ b/examples/release1/oa/behavior/process_oa_assessment_procedure.arcadia @@ -0,0 +1,25 @@ +oa_process oa_assessment_procedure "Assess an alarm procedure" id "022d3cfa-bbaf-5e13-924a-8e55099b8745" { + description ""; + parent none; + steps [@oa_assessment_action]; +} + +oa_occurrence oa_assessment_action "Assess pressure alarm" id "942975b8-0bef-5ae8-9fcb-c37497833fb0" { + body { + Action { + action @oa_assess; + lifeline none; + }; + }; + description ""; + parent @oa_assessment_procedure; + postcondition ""; + precondition ""; +} + +chain_involvement oa_assessment_delivery "Chain involvement: Assess an alarm" id "628cb4fc-fe63-5f6b-b364-6268f26b5740" { + description ""; + source @oa_assessment_procedure; + target @oa_triage; +} + diff --git a/examples/release1/oa/behavior/process_oa_field_procedure.arcadia b/examples/release1/oa/behavior/process_oa_field_procedure.arcadia new file mode 100644 index 00000000..d639717f --- /dev/null +++ b/examples/release1/oa/behavior/process_oa_field_procedure.arcadia @@ -0,0 +1,67 @@ +oa_process oa_field_procedure "Field repair procedure" id "5d3af417-244c-54bb-9588-16daedda4e1f" { + description ""; + parent none; + steps [@oa_request_step, @oa_repair_step, @oa_report_step]; +} + +oa_occurrence oa_repair_step "Repair the valve" id "c59432fe-6092-5ee0-908d-9dbf39476471" { + body { + Action { + action @oa_repair; + lifeline none; + }; + }; + description ""; + parent @oa_field_procedure; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_report_step "Report the measured result" id "df2fd3f8-6bc2-5380-a13f-36643ed35ac7" { + body { + Exchange { + exchange @oa_repair_report; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @oa_field_procedure; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_request_step "Request the field repair" id "5ba8079c-2dbe-5e98-ae82-2b60c6020a02" { + body { + Exchange { + exchange @oa_repair_request; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @oa_field_procedure; + postcondition ""; + precondition ""; +} + +sequence_link oa_repair_then_report "Sequence link: Report the measured result" id "c8196d35-2168-5fef-8b03-ba0d139bc00f" { + description ""; + guard ""; + source @oa_repair_step; + target @oa_report_step; +} + +sequence_link oa_request_then_repair "Sequence link: Repair the valve" id "3f54d890-4ca5-5e36-873b-abb5c92d578c" { + description ""; + guard ""; + source @oa_request_step; + target @oa_repair_step; +} + diff --git a/examples/release1/oa/behavior/process_oa_response.arcadia b/examples/release1/oa/behavior/process_oa_response.arcadia new file mode 100644 index 00000000..b4cf4c29 --- /dev/null +++ b/examples/release1/oa/behavior/process_oa_response.arcadia @@ -0,0 +1,128 @@ +oa_process oa_response "Handle a pressure alarm" id "58bae2b0-f560-50f2-818d-19091d22d18a" { + description ""; + parent none; + steps [@oa_assess_step, @oa_decision, @oa_reset_step, @oa_use_field, @oa_merge, @oa_confirm_step]; +} + +oa_occurrence oa_assess_step "Assess the alarm" id "0b07b9ed-492d-57f6-b0b1-ce8a709238a7" { + body { + SequenceReference { + sequence @oa_assessment_procedure; + }; + }; + description ""; + parent @oa_response; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_confirm_step "Confirm safe pressure" id "e00594a3-6122-5a6b-b3c2-ecb5506c741d" { + body { + Action { + action @oa_confirm; + lifeline none; + }; + }; + description ""; + parent @oa_response; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_decision "Can a remote reset safely recover pressure?" id "b280c6ca-e5f9-5424-a5c1-5676e01c2371" { + body { + Control { + kind Choice; + }; + }; + description ""; + parent @oa_response; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_merge "Recovery path completed" id "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d" { + body { + Control { + kind Merge; + }; + }; + description ""; + parent @oa_response; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_reset_step "Try the authorized remote reset" id "d2026fc2-74a8-5e22-9942-ff3eb68a3677" { + body { + Action { + action @oa_reset; + lifeline none; + }; + }; + description ""; + parent @oa_response; + postcondition ""; + precondition ""; +} + +oa_occurrence oa_use_field "Use the field repair procedure" id "fc6763bf-a534-54bf-afcd-57ab285ff07d" { + body { + SequenceReference { + sequence @oa_field_procedure; + }; + }; + description ""; + parent @oa_response; + postcondition ""; + precondition ""; +} + +sequence_link oa_assess_then_decide "Continue" id "109e2d6b-7e86-5934-93a5-fab56eced84e" { + description ""; + guard ""; + source @oa_assess_step; + target @oa_decision; +} + +sequence_link oa_field_path "else" id "c1adbbd0-a762-5ee3-8b9a-5c18af377771" { + description ""; + guard "else"; + source @oa_decision; + target @oa_use_field; +} + +sequence_link oa_remote_path "remote reset is authorized" id "76218d89-68fd-531d-b35b-90c5b769a5ef" { + description ""; + guard "remote reset is authorized"; + source @oa_decision; + target @oa_reset_step; +} + +sequence_link oa_confirm_path "Continue" id "229ee94f-d147-50f6-9769-b5bd0c03a084" { + description ""; + guard ""; + source @oa_merge; + target @oa_confirm_step; +} + +sequence_link oa_remote_merge "Continue" id "bac43d3b-910b-5bdc-be9b-f78cf973e1ed" { + description ""; + guard ""; + source @oa_reset_step; + target @oa_merge; +} + +chain_involvement oa_response_delivery "Chain involvement: Restore a safe supply" id "c7c248f8-28b8-55be-9023-0991d44021e3" { + description ""; + source @oa_response; + target @oa_restore; +} + +sequence_link oa_field_merge "Continue" id "c8593670-8906-530f-b6a2-3ada77a9c219" { + description ""; + guard ""; + source @oa_use_field; + target @oa_merge; +} + diff --git a/examples/release1/oa/behavior/scenario_oa_conversation.arcadia b/examples/release1/oa/behavior/scenario_oa_conversation.arcadia new file mode 100644 index 00000000..89470e88 --- /dev/null +++ b/examples/release1/oa/behavior/scenario_oa_conversation.arcadia @@ -0,0 +1,64 @@ +oa_scenario oa_conversation "Repair request and measured reply" id "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" { + context @oa_restore; + description ""; + parent none; + postcondition "The controller has received the measured pressure after repair"; + precondition "A pressure alarm has been assessed"; + steps [@oa_call, @oa_reply]; + viewpoint ParticipantInteraction; +} + +oa_occurrence oa_call "Request valve repair" id "b97437c6-cd7a-5a05-b537-b30a17dd936a" { + body { + Exchange { + exchange @oa_contact; + message_kind Call; + receiver @oa_responder; + reply_to none; + sender @oa_caller; + values [{ + expression "420.0"; + field @oa_pressure_parameter; + path []; + }]; + }; + }; + description ""; + parent @oa_conversation; + postcondition ""; + precondition ""; +} + +oa_lifeline oa_caller "Controller" id "41859a65-ded2-547b-ab03-14e2fa740cf9" { + description ""; + parent @oa_conversation; + represented @oa_control; +} + +oa_occurrence oa_reply "Return pressure after repair" id "cc2d497a-fd15-5455-9ee5-e70eb95811d8" { + body { + Exchange { + exchange @oa_contact; + message_kind Reply; + receiver @oa_caller; + reply_to @oa_call; + sender @oa_responder; + values [{ + expression "320.0"; + field @oa_pressure_parameter; + path []; + }]; + }; + }; + description ""; + parent @oa_conversation; + postcondition ""; + precondition ""; +} + +oa_lifeline oa_responder "Technician" id "fc81c5f0-0442-5b88-992b-e5b10f351c59" { + description ""; + parent @oa_conversation; + represented @oa_technician; +} + diff --git a/examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia b/examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia new file mode 100644 index 00000000..33c9aad1 --- /dev/null +++ b/examples/release1/oa/behavior/scenario_oa_full_scenario.arcadia @@ -0,0 +1,41 @@ +oa_scenario oa_full_scenario "Restore supply through field repair" id "d6f881a4-95da-54e9-ad97-1b8965532629" { + context @oa_restore; + description ""; + parent none; + postcondition "The controller has received the measured pressure after repair"; + precondition "A pressure alarm has been assessed"; + steps [@oa_use_conversation]; + viewpoint ParticipantInteraction; +} + +oa_lifeline oa_outer_controller "Controller" id "2011b3ac-614b-5a47-8891-7ff81965f425" { + description ""; + parent @oa_full_scenario; + represented @oa_control; +} + +oa_lifeline oa_outer_technician "Technician" id "fffff882-78fd-5415-928d-c1ef7fac6576" { + description ""; + parent @oa_full_scenario; + represented @oa_technician; +} + +oa_occurrence oa_use_conversation "Use the reviewed repair conversation" id "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5" { + body { + ScenarioUse { + bindings [{ + local @oa_outer_controller; + referenced @oa_caller; + }, { + local @oa_outer_technician; + referenced @oa_responder; + }]; + scenario @oa_conversation; + }; + }; + description ""; + parent @oa_full_scenario; + postcondition ""; + precondition ""; +} + diff --git a/examples/release1/oa/behavior/state_machine_oa_monitor.arcadia b/examples/release1/oa/behavior/state_machine_oa_monitor.arcadia new file mode 100644 index 00000000..aeaa90cf --- /dev/null +++ b/examples/release1/oa/behavior/state_machine_oa_monitor.arcadia @@ -0,0 +1,80 @@ +oa_state_machine oa_monitor "Pressure supervision" id "1970608f-85d1-5234-ac86-5e2b0df27dc3" { + description ""; + parent @oa_control; +} + +oa_region oa_monitor_region "Supervision cycle" id "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" { + description ""; + parent @oa_monitor; +} + +oa_initial oa_monitor_initial "Start" id "a7ffb869-9dde-5e76-b276-c719c5c143a9" { + description ""; + do_activity ""; + entry ""; + exit ""; + kind Initial; + parent @oa_monitor_region; +} + +oa_state oa_monitoring "Monitoring" id "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae" { + description ""; + do_activity ""; + entry ""; + exit ""; + kind State; + parent @oa_monitor_region; +} + +oa_state oa_waiting "Awaiting repair" id "58c15833-6e75-523a-b57f-a88f19f6d672" { + description ""; + do_activity ""; + entry ""; + exit ""; + kind State; + parent @oa_monitor_region; +} + +state_transition oa_start_monitoring "State transition: Monitoring" id "2b00270c-79a5-52a5-af96-33346bea531b" { + description ""; + effect ""; + effects []; + guard ""; + source @oa_monitor_initial; + target @oa_monitoring; + transition_kind External; + trigger ""; + triggers []; +} + +state_transition oa_alarm_transition "State transition: Awaiting repair" id "fbde9d5e-c7e9-55db-a2be-497345c491b3" { + description ""; + effect ""; + effects []; + guard ""; + source @oa_monitoring; + target @oa_waiting; + transition_kind External; + trigger ""; + triggers [@oa_high_pressure]; +} + +state_action oa_assess_on_entry "Assess when repair is needed" id "7fa37e63-db65-5aa7-88f2-31ff4347a787" { + description ""; + phase Entry; + source @oa_waiting; + target @oa_assess; +} + +state_transition oa_normal_transition "State transition: Monitoring" id "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97" { + description ""; + effect ""; + effects []; + guard "repair confirmed and pressure is safe"; + source @oa_waiting; + target @oa_monitoring; + transition_kind External; + trigger ""; + triggers []; +} + diff --git a/examples/release1/oa/capabilities.arcadia b/examples/release1/oa/capabilities.arcadia new file mode 100644 index 00000000..77c2ab08 --- /dev/null +++ b/examples/release1/oa/capabilities.arcadia @@ -0,0 +1,27 @@ +oa_mission oa_mission "Maintain safe water supply" id "c9c61f50-06e0-5d17-973d-dcc57cd5cc90" { + description ""; + parent none; +} + +oa_capability oa_restore "Restore a safe supply" id "3dfee23a-148b-5cec-ad2d-b774e84ea5bb" { + description ""; + parent none; +} + +oa_capability oa_triage "Assess an alarm" id "6a30b763-cc51-546d-907f-5c63657cfd03" { + description ""; + parent none; +} + +mission_capability oa_mission_delivery "Mission capability: Restore a safe supply" id "1f8b1898-65f6-5360-99cf-b78a71bd50ec" { + description ""; + source @oa_mission; + target @oa_restore; +} + +capability_include oa_include_triage "Capability include: Assess an alarm" id "e21d9a54-ae8e-5618-a4f8-67ee22b92638" { + description ""; + source @oa_restore; + target @oa_triage; +} + diff --git a/examples/release1/oa/model.arcadia b/examples/release1/oa/model.arcadia new file mode 100644 index 00000000..b3f4a60a --- /dev/null +++ b/examples/release1/oa/model.arcadia @@ -0,0 +1,24 @@ +oa_exchange_category oa_contact_category "Field contact" id "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771" { + description ""; + exchanges [@oa_contact]; + family "Participant"; + parent none; +} + +oa_event oa_high_pressure "Pressure alarm detected" id "65d6b8d6-271a-5c84-8b42-abbe310c955e" { + body { + Change { + expression "measured_pressure > operating_limit"; + }; + }; + description ""; + parent none; +} + +oa_exchange_category oa_repair_traffic "Repair work exchanges" id "9349b353-53f2-56f8-b571-a05b09335cdf" { + description ""; + exchanges [@oa_repair_request, @oa_repair_report]; + family "Work"; + parent none; +} + diff --git a/examples/release1/oa/participants.arcadia b/examples/release1/oa/participants.arcadia new file mode 100644 index 00000000..ed901190 --- /dev/null +++ b/examples/release1/oa/participants.arcadia @@ -0,0 +1,60 @@ +oa_entity oa_control "Control room" id "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" { + description ""; + parent none; +} + +oa_actor oa_technician "Field technician" id "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" { + description ""; + parent none; +} + +allocation oa_assess_allocation "Allocation: Assess pressure alarm" id "594c61cd-94e0-5c07-890d-edfd520d7df9" { + description ""; + source @oa_control; + target @oa_assess; +} + +allocation oa_confirm_allocation "Allocation: Confirm safe pressure" id "bfd4326e-ea65-5269-b5e6-e063a355d1e9" { + description ""; + source @oa_control; + target @oa_confirm; +} + +communication_mean oa_contact "Technician service conversation" id "27b47887-baf0-518b-bf87-f5acbf7d0b93" { + description ""; + items [@oa_repair_operation]; + oriented false; + source @oa_control; + target @oa_technician; +} + +exchange_allocation oa_report_carriage "Exchange allocation: Report repair result" id "4549d3fa-5ab9-52a9-b4f9-3a78fc9d625a" { + description ""; + source @oa_contact; + target @oa_repair_report; +} + +exchange_allocation oa_request_carriage "Exchange allocation: Request field repair" id "094b888c-450c-5c4f-971a-7d75db059574" { + description ""; + source @oa_contact; + target @oa_repair_request; +} + +mission_involvement oa_mission_participant "Mission involvement: Maintain safe water supply" id "0c4c9e58-ff32-5c00-b82a-d408a4be0183" { + description ""; + source @oa_control; + target @oa_mission; +} + +allocation oa_reset_allocation "Allocation: Reset valve remotely" id "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a" { + description ""; + source @oa_control; + target @oa_reset; +} + +allocation oa_repair_allocation "Allocation: Repair valve in the field" id "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2" { + description ""; + source @oa_technician; + target @oa_repair; +} + diff --git a/examples/release1/oa/types.arcadia b/examples/release1/oa/types.arcadia new file mode 100644 index 00000000..c285895b --- /dev/null +++ b/examples/release1/oa/types.arcadia @@ -0,0 +1,35 @@ +oa_data_type oa_pressure "Measured pressure" id "e7824764-5db9-5384-8198-2fbaea2b10b2" { + default none; + definition { + Real { + maximum 600.0; + minimum 0.0; + }; + }; + description ""; + parent none; + unit "kPa"; +} + +oa_exchange_item oa_repair_operation "Valve repair" id "d736c7d6-d369-550a-8ba4-dae223e40bb3" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; +} + +oa_data_field oa_pressure_parameter "Pressure before / after repair" id "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7" { + default none; + description ""; + direction InOut; + maximum 1; + minimum 1; + parent @oa_repair_operation; + storage Value; + type_ref { + Named @oa_pressure; + }; + unit ""; +} + diff --git a/examples/release1/project.arcadia b/examples/release1/project.arcadia new file mode 100644 index 00000000..c8fab0d7 --- /dev/null +++ b/examples/release1/project.arcadia @@ -0,0 +1,33 @@ +arcadia 6; +project pressure_restoration "Restore safe water pressure — release 1 walkthrough" id "2786d72f-5763-5f33-a57f-2773a4ca1a6c" { + description "A controller either resets a valve remotely or requests field repair. Compare the operational need with the explicitly chosen coordination-service boundary. Start with README.md beside project.arcadia. Units and guards are descriptive; this model does not simulate a real water network."; + include "oa/activities/activity_oa_restore_work.arcadia"; + include "oa/behavior/process_oa_assessment_procedure.arcadia"; + include "oa/behavior/process_oa_field_procedure.arcadia"; + include "oa/behavior/process_oa_response.arcadia"; + include "oa/behavior/scenario_oa_conversation.arcadia"; + include "oa/behavior/scenario_oa_full_scenario.arcadia"; + include "oa/behavior/state_machine_oa_monitor.arcadia"; + include "oa/capabilities.arcadia"; + include "oa/model.arcadia"; + include "oa/participants.arcadia"; + include "oa/types.arcadia"; + include "sa/behavior/process_sa_assessment_procedure.arcadia"; + include "sa/behavior/process_sa_field_procedure.arcadia"; + include "sa/behavior/process_sa_response.arcadia"; + include "sa/behavior/scenario_sa_conversation.arcadia"; + include "sa/behavior/scenario_sa_full_scenario.arcadia"; + include "sa/behavior/state_machine_sa_monitor.arcadia"; + include "sa/capabilities.arcadia"; + include "sa/functions/function_sa_audit_thresholds.arcadia"; + include "sa/functions/function_sa_restore_work.arcadia"; + include "sa/model.arcadia"; + include "sa/participants.arcadia"; + include "sa/types.arcadia"; + include "traceability.arcadia"; + include "views/diagram_oa_allocation_view.arcadia"; + include "views/diagram_sa_allocation_view.arcadia"; + include "views/diagram_sa_capability_view.arcadia"; + include "views/diagram_sa_service_view.arcadia"; + include "views/diagram_sa_state_view.arcadia"; +} diff --git a/examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia b/examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia new file mode 100644 index 00000000..ff30c34f --- /dev/null +++ b/examples/release1/sa/behavior/process_sa_assessment_procedure.arcadia @@ -0,0 +1,25 @@ +sa_functional_chain sa_assessment_procedure "Assess an alarm procedure" id "acd5a018-f559-5298-a40d-1cf6e1353e74" { + description ""; + parent none; + steps [@sa_assessment_action]; +} + +sa_occurrence sa_assessment_action "Assess pressure alarm" id "bae5b192-5b8e-51c6-82ae-2331af0d33bf" { + body { + Action { + action @sa_assess; + lifeline none; + }; + }; + description ""; + parent @sa_assessment_procedure; + postcondition ""; + precondition ""; +} + +chain_involvement sa_assessment_delivery "Chain involvement: Assess an alarm" id "a6d5a6be-fa6e-5fa0-a309-08e87ba40c57" { + description ""; + source @sa_assessment_procedure; + target @sa_triage; +} + diff --git a/examples/release1/sa/behavior/process_sa_field_procedure.arcadia b/examples/release1/sa/behavior/process_sa_field_procedure.arcadia new file mode 100644 index 00000000..b6de2a32 --- /dev/null +++ b/examples/release1/sa/behavior/process_sa_field_procedure.arcadia @@ -0,0 +1,67 @@ +sa_functional_chain sa_field_procedure "Field repair procedure" id "93eb6479-54e9-59cb-ac7b-16912347bfc0" { + description ""; + parent none; + steps [@sa_request_step, @sa_repair_step, @sa_report_step]; +} + +sa_occurrence sa_repair_step "Repair the valve" id "0f195fea-3493-5ebd-966d-da9eddeb05d7" { + body { + Action { + action @sa_repair; + lifeline none; + }; + }; + description ""; + parent @sa_field_procedure; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_report_step "Report the measured result" id "ea32e4ec-8bdc-57ea-8a90-7b09ba4f0eac" { + body { + Exchange { + exchange @sa_repair_report; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @sa_field_procedure; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_request_step "Request the field repair" id "40d94322-baa2-5c81-b261-ff47cdc33782" { + body { + Exchange { + exchange @sa_repair_request; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @sa_field_procedure; + postcondition ""; + precondition ""; +} + +sequence_link sa_repair_then_report "Sequence link: Report the measured result" id "7fb30b1d-195c-5f0e-a19c-a536f519a899" { + description ""; + guard ""; + source @sa_repair_step; + target @sa_report_step; +} + +sequence_link sa_request_then_repair "Sequence link: Repair the valve" id "6a6d968f-26d6-5dbb-b721-5a27d362779a" { + description ""; + guard ""; + source @sa_request_step; + target @sa_repair_step; +} + diff --git a/examples/release1/sa/behavior/process_sa_response.arcadia b/examples/release1/sa/behavior/process_sa_response.arcadia new file mode 100644 index 00000000..241fb132 --- /dev/null +++ b/examples/release1/sa/behavior/process_sa_response.arcadia @@ -0,0 +1,128 @@ +sa_functional_chain sa_response "Handle a pressure alarm" id "5139ce73-56fd-5875-ba3a-e569fc890231" { + description ""; + parent none; + steps [@sa_assess_step, @sa_decision, @sa_reset_step, @sa_use_field, @sa_merge, @sa_confirm_step]; +} + +sa_occurrence sa_assess_step "Assess the alarm" id "9024243c-ccc1-5edb-8477-2aa16e5e9a50" { + body { + SequenceReference { + sequence @sa_assessment_procedure; + }; + }; + description ""; + parent @sa_response; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_confirm_step "Confirm safe pressure" id "47bec4f9-d6d0-5ed1-adca-fa0d960dc416" { + body { + Action { + action @sa_confirm; + lifeline none; + }; + }; + description ""; + parent @sa_response; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_decision "Can a remote reset safely recover pressure?" id "30c8fad9-a49b-5692-9542-3148e76afd7b" { + body { + Control { + kind Choice; + }; + }; + description ""; + parent @sa_response; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_merge "Recovery path completed" id "4ed0dd9d-c9e0-5652-aedf-5f0ab4328a07" { + body { + Control { + kind Merge; + }; + }; + description ""; + parent @sa_response; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_reset_step "Try the authorized remote reset" id "bfa2ef0b-ce68-55c2-a9e5-31ea8a74fce6" { + body { + Action { + action @sa_reset; + lifeline none; + }; + }; + description ""; + parent @sa_response; + postcondition ""; + precondition ""; +} + +sa_occurrence sa_use_field "Use the field repair procedure" id "d5e61c5d-510b-5091-a878-da46f3a9e152" { + body { + SequenceReference { + sequence @sa_field_procedure; + }; + }; + description ""; + parent @sa_response; + postcondition ""; + precondition ""; +} + +sequence_link sa_assess_then_decide "Continue" id "9d4a131f-36ad-5125-b21f-c3dd033b8c82" { + description ""; + guard ""; + source @sa_assess_step; + target @sa_decision; +} + +sequence_link sa_field_path "else" id "1bbd5957-1173-51d8-97e3-ba4f084227ae" { + description ""; + guard "else"; + source @sa_decision; + target @sa_use_field; +} + +sequence_link sa_remote_path "remote reset is authorized" id "d8b6710a-e713-5979-934f-e5ca7d155bb4" { + description ""; + guard "remote reset is authorized"; + source @sa_decision; + target @sa_reset_step; +} + +sequence_link sa_confirm_path "Continue" id "1e738f27-a3a1-526d-b330-6384088b9689" { + description ""; + guard ""; + source @sa_merge; + target @sa_confirm_step; +} + +sequence_link sa_remote_merge "Continue" id "e5279265-8eac-52e8-98dc-571b20475e1f" { + description ""; + guard ""; + source @sa_reset_step; + target @sa_merge; +} + +chain_involvement sa_response_delivery "Chain involvement: Restore a safe supply" id "66efc484-53c7-51a3-89e9-422429344b38" { + description ""; + source @sa_response; + target @sa_restore; +} + +sequence_link sa_field_merge "Continue" id "0f6ac3fb-4ad6-581e-ad14-336472072d5e" { + description ""; + guard ""; + source @sa_use_field; + target @sa_merge; +} + diff --git a/examples/release1/sa/behavior/scenario_sa_conversation.arcadia b/examples/release1/sa/behavior/scenario_sa_conversation.arcadia new file mode 100644 index 00000000..4e324730 --- /dev/null +++ b/examples/release1/sa/behavior/scenario_sa_conversation.arcadia @@ -0,0 +1,73 @@ +sa_scenario sa_conversation "Repair request and measured reply" id "730b27ca-b039-5a3b-8dc6-03418f9061b3" { + context @sa_restore; + description ""; + parent none; + postcondition "The controller has received the measured pressure after repair"; + precondition "A pressure alarm has been assessed"; + steps [@sa_call, @sa_reply]; + viewpoint BoundaryInterface; +} + +sa_occurrence sa_call "Request valve repair" id "8bab4783-a098-5984-9e36-493957caf26c" { + body { + Exchange { + exchange @sa_contact; + message_kind Call; + receiver @sa_responder; + reply_to none; + sender @sa_caller; + values [{ + expression "420.0"; + field @sa_pressure_parameter; + path []; + }]; + }; + }; + description ""; + parent @sa_conversation; + postcondition ""; + precondition ""; +} + +sa_lifeline sa_caller "Controller" id "98c61788-2fee-51f4-8cf9-de48e8c70e9e" { + description ""; + parent @sa_conversation; + represented @sa_control; +} + +sa_occurrence sa_reply "Return pressure after repair" id "4e1c0a56-ce80-5f13-a93a-6956de54880c" { + body { + Exchange { + exchange @sa_contact; + message_kind Reply; + receiver @sa_caller; + reply_to @sa_call; + sender @sa_responder; + values [{ + expression "320.0"; + field @sa_pressure_parameter; + path []; + }]; + }; + }; + description ""; + parent @sa_conversation; + postcondition ""; + precondition ""; +} + +sa_lifeline sa_responder "Technician" id "6249b54a-e396-5338-82b9-89266c55670d" { + description ""; + parent @sa_conversation; + represented @sa_technician; +} + +artifact repair_test_evidence "Repair conversation acceptance notes" id "f5922bf7-05d8-590f-86e9-1d09f37f4fa6" { + description "A local teaching artifact, not proof of a real valve's safety."; + fragment "acceptance"; + owner @sa_conversation; + relation Verifies; + revision "lesson-1"; + uri "evidence/repair-check.md"; +} + diff --git a/examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia b/examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia new file mode 100644 index 00000000..b88e5d12 --- /dev/null +++ b/examples/release1/sa/behavior/scenario_sa_full_scenario.arcadia @@ -0,0 +1,41 @@ +sa_scenario sa_full_scenario "Restore supply through field repair" id "f746a1cd-ff9f-55c0-b325-01c03d2f4fae" { + context @sa_restore; + description ""; + parent none; + postcondition "The controller has received the measured pressure after repair"; + precondition "A pressure alarm has been assessed"; + steps [@sa_use_conversation]; + viewpoint BoundaryInterface; +} + +sa_lifeline sa_outer_controller "Controller" id "1edfcf9e-9bfa-51f1-80d5-62b8fd8bb023" { + description ""; + parent @sa_full_scenario; + represented @sa_control; +} + +sa_lifeline sa_outer_technician "Technician" id "155e4fe4-9e3b-5f4e-b886-28abcc3f086b" { + description ""; + parent @sa_full_scenario; + represented @sa_technician; +} + +sa_occurrence sa_use_conversation "Use the reviewed repair conversation" id "1c9da843-e2d3-5591-8e3f-db083a42867e" { + body { + ScenarioUse { + bindings [{ + local @sa_outer_controller; + referenced @sa_caller; + }, { + local @sa_outer_technician; + referenced @sa_responder; + }]; + scenario @sa_conversation; + }; + }; + description ""; + parent @sa_full_scenario; + postcondition ""; + precondition ""; +} + diff --git a/examples/release1/sa/behavior/state_machine_sa_monitor.arcadia b/examples/release1/sa/behavior/state_machine_sa_monitor.arcadia new file mode 100644 index 00000000..46949f1b --- /dev/null +++ b/examples/release1/sa/behavior/state_machine_sa_monitor.arcadia @@ -0,0 +1,80 @@ +sa_state_machine sa_monitor "Pressure supervision" id "132b3f28-3f88-5327-9868-8c8b645ed412" { + description ""; + parent @sa_control; +} + +sa_region sa_monitor_region "Supervision cycle" id "3ab11e24-4140-58fc-b3fa-da5f51c7fe2c" { + description ""; + parent @sa_monitor; +} + +sa_initial sa_monitor_initial "Start" id "57eb5348-14d8-560e-996a-8dbf993243e2" { + description ""; + do_activity ""; + entry ""; + exit ""; + kind Initial; + parent @sa_monitor_region; +} + +sa_state sa_monitoring "Monitoring" id "8af21f9b-c21e-5aae-af0d-0958840f6655" { + description ""; + do_activity ""; + entry ""; + exit ""; + kind State; + parent @sa_monitor_region; +} + +sa_state sa_waiting "Awaiting repair" id "a6bb10b1-f1e5-5b31-8afd-5bad5a7b6c37" { + description ""; + do_activity ""; + entry ""; + exit ""; + kind State; + parent @sa_monitor_region; +} + +state_transition sa_start_monitoring "State transition: Monitoring" id "47730532-0372-5cc5-9d6d-0b8953aebdd8" { + description ""; + effect ""; + effects []; + guard ""; + source @sa_monitor_initial; + target @sa_monitoring; + transition_kind External; + trigger ""; + triggers []; +} + +state_transition sa_alarm_transition "State transition: Awaiting repair" id "13c3bfa4-9254-5ab2-b96f-f65f402adf81" { + description ""; + effect ""; + effects []; + guard ""; + source @sa_monitoring; + target @sa_waiting; + transition_kind External; + trigger ""; + triggers [@sa_high_pressure]; +} + +state_action sa_assess_on_entry "Assess when repair is needed" id "a887f96a-37cb-5891-8070-011fa276e5b5" { + description ""; + phase Entry; + source @sa_waiting; + target @sa_assess; +} + +state_transition sa_normal_transition "State transition: Monitoring" id "6e2c1b47-764c-577a-b325-ff66dd2a2768" { + description ""; + effect ""; + effects []; + guard "repair confirmed and pressure is safe"; + source @sa_waiting; + target @sa_monitoring; + transition_kind External; + trigger ""; + triggers []; +} + diff --git a/examples/release1/sa/capabilities.arcadia b/examples/release1/sa/capabilities.arcadia new file mode 100644 index 00000000..4b5c4210 --- /dev/null +++ b/examples/release1/sa/capabilities.arcadia @@ -0,0 +1,27 @@ +sa_mission sa_mission "Maintain safe water supply" id "5ad034b1-084d-5273-9e70-971671766132" { + description ""; + parent none; +} + +sa_capability sa_restore "Restore a safe supply" id "81259051-370a-512d-a3b5-6420a6f933b6" { + description ""; + parent none; +} + +sa_capability sa_triage "Assess an alarm" id "bad8fa7c-f634-55fd-98d3-e1eb2da05d8f" { + description ""; + parent none; +} + +mission_capability sa_mission_delivery "Mission capability: Restore a safe supply" id "a0902e0f-3d18-576a-9aca-975a136dfdd7" { + description ""; + source @sa_mission; + target @sa_restore; +} + +capability_include sa_include_triage "Capability include: Assess an alarm" id "f3fe7690-734a-5877-8b57-a6599a4033c4" { + description ""; + source @sa_restore; + target @sa_triage; +} + diff --git a/examples/release1/sa/functions/function_sa_audit_thresholds.arcadia b/examples/release1/sa/functions/function_sa_audit_thresholds.arcadia new file mode 100644 index 00000000..2ec039aa --- /dev/null +++ b/examples/release1/sa/functions/function_sa_audit_thresholds.arcadia @@ -0,0 +1,5 @@ +sa_function sa_audit_thresholds "Audit pressure thresholds" id "4149a872-3440-59de-9879-8981e78d27ab" { + description "Deliberately unfinished: assigned to the service, but no behavior context yet. Model checks should expose this one work_without_behavior_context warning."; + parent none; +} + diff --git a/examples/release1/sa/functions/function_sa_restore_work.arcadia b/examples/release1/sa/functions/function_sa_restore_work.arcadia new file mode 100644 index 00000000..772ec0f1 --- /dev/null +++ b/examples/release1/sa/functions/function_sa_restore_work.arcadia @@ -0,0 +1,93 @@ +sa_function sa_restore_work "Restore safe pressure" id "58867e7d-916e-5ebc-9e4c-8dbed1314be3" { + description ""; + parent none; +} + +sa_function sa_assess "Assess pressure alarm" id "dddae020-a444-5d48-acc8-139d2696a672" { + description ""; + parent @sa_restore_work; +} + +sa_port sa_repair_request_out "Request field repair output" id "24a6850e-b819-57e0-af88-ca4ab76581b7" { + contract { + FunctionOutput { + outgoing_items [@sa_repair_operation]; + }; + }; + description ""; + legacy none; + parent @sa_assess; +} + +sa_function sa_confirm "Confirm safe pressure" id "d75eb3fa-a27d-52d5-8782-8b342b3865f6" { + description ""; + parent @sa_restore_work; +} + +sa_port sa_repair_report_in "Report repair result input" id "391af7c4-b07d-53a7-8ddd-5c81461fc4f8" { + contract { + FunctionInput { + incoming_items [@sa_repair_operation]; + }; + }; + description ""; + legacy none; + parent @sa_confirm; +} + +sa_function sa_repair "Repair valve in the field" id "9497e10f-04c4-5586-8738-2464a6f00bd4" { + description ""; + parent @sa_restore_work; +} + +sa_port sa_repair_report_out "Report repair result output" id "994faed7-dbc8-5b96-9d62-7604ac0fbc3a" { + contract { + FunctionOutput { + outgoing_items [@sa_repair_operation]; + }; + }; + description ""; + legacy none; + parent @sa_repair; +} + +sa_port sa_repair_request_in "Request field repair input" id "d2113fb0-a531-54ef-9484-13951b00ab8f" { + contract { + FunctionInput { + incoming_items [@sa_repair_operation]; + }; + }; + description ""; + legacy none; + parent @sa_repair; +} + +sa_function sa_reset "Reset valve remotely" id "09342ccb-044b-5483-ac58-87dc3cf7831c" { + description ""; + parent @sa_restore_work; +} + +capability_involvement sa_assessment_support "Capability involvement: Assess an alarm" id "93eaad8d-83b7-5980-b9bb-86c9e3f31da4" { + description ""; + source @sa_assess; + target @sa_triage; +} + +functional_exchange sa_repair_request "Request field repair" id "93474111-f23e-56c1-b61b-b7c2396b91bd" { + description ""; + items [@sa_repair_operation]; + source @sa_assess; + source_port @sa_repair_request_out; + target @sa_repair; + target_port @sa_repair_request_in; +} + +functional_exchange sa_repair_report "Report repair result" id "684eadc3-e670-5779-a60f-bbc9a5bfb062" { + description ""; + items [@sa_repair_operation]; + source @sa_repair; + source_port @sa_repair_report_out; + target @sa_confirm; + target_port @sa_repair_report_in; +} + diff --git a/examples/release1/sa/model.arcadia b/examples/release1/sa/model.arcadia new file mode 100644 index 00000000..0856d908 --- /dev/null +++ b/examples/release1/sa/model.arcadia @@ -0,0 +1,24 @@ +sa_exchange_category sa_contact_category "Field contact" id "781cbe75-410f-5cc5-aeb8-a43491f77b99" { + description ""; + exchanges [@sa_contact]; + family "Participant"; + parent none; +} + +sa_event sa_high_pressure "Pressure alarm detected" id "01490fa8-2f01-5ac8-9722-d55d45414379" { + body { + Change { + expression "measured_pressure > operating_limit"; + }; + }; + description ""; + parent none; +} + +sa_exchange_category sa_repair_traffic "Repair work exchanges" id "a9a47e48-100c-57a8-95f0-7fe24da94f94" { + description ""; + exchanges [@sa_repair_request, @sa_repair_report]; + family "Work"; + parent none; +} + diff --git a/examples/release1/sa/participants.arcadia b/examples/release1/sa/participants.arcadia new file mode 100644 index 00000000..4b9c5558 --- /dev/null +++ b/examples/release1/sa/participants.arcadia @@ -0,0 +1,126 @@ +sa_system sa_control "Pressure coordination service" id "435bf87c-73fd-5e4d-978a-4c471bc9eaf0" { + description ""; + parent none; +} + +sa_port sa_control_port "Repair service client" id "98d411e7-7c65-5c3f-b543-8bf0f2522ea2" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description "Explicitly chosen Standard component port for the repair service."; + legacy none; + parent @sa_control; +} + +sa_actor sa_technician "Field technician" id "c05b9422-b9da-5de5-b21d-07ad23bd2b8a" { + description ""; + parent none; +} + +sa_port sa_technician_port "Repair service provider" id "e6d69cbd-99d7-596b-bbe3-dada6118a600" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description "Explicitly chosen Standard component port for the repair service."; + legacy none; + parent @sa_technician; +} + +allocation sa_assess_allocation "Allocation: Assess pressure alarm" id "9b9356d7-2db2-5a94-a47e-ea892cf02afe" { + description ""; + source @sa_control; + target @sa_assess; +} + +allocation sa_audit_allocation "Allocation: Audit pressure thresholds" id "1b245065-7242-5d81-8472-aaeb932054c9" { + description ""; + source @sa_control; + target @sa_audit_thresholds; +} + +allocation sa_confirm_allocation "Allocation: Confirm safe pressure" id "b76bf8de-2fa4-563f-a540-dd30f30e6c49" { + description ""; + source @sa_control; + target @sa_confirm; +} + +interface_requirement sa_client_requires "Interface requirement: Valve repair service" id "4f85337b-f6eb-533c-a45a-9a5d1d9567ca" { + description ""; + source @sa_control_port; + target @sa_repair_interface; +} + +boundary_exchange sa_contact "Technician service conversation" id "63009b5f-539e-5914-b630-ec6b7fefb4d5" { + description ""; + items [@sa_repair_operation]; + oriented false; + source @sa_control_port; + target @sa_technician_port; +} + +exchange_allocation sa_report_carriage "Exchange allocation: Report repair result" id "ff9897b3-a23a-5994-a5b3-99a5b4d34ceb" { + description ""; + source @sa_contact; + target @sa_repair_report; +} + +exchange_allocation sa_request_carriage "Exchange allocation: Request field repair" id "4c4c4851-5a3d-5bb9-94df-b19f9cf39819" { + description ""; + source @sa_contact; + target @sa_repair_request; +} + +port_allocation sa_port_report_in "Port allocation: Report repair result input" id "568c5c01-de21-52a2-83bd-afdddc158085" { + description ""; + source @sa_control_port; + target @sa_repair_report_in; +} + +port_allocation sa_port_request_out "Port allocation: Request field repair output" id "55ec4dbf-bd17-5b0c-b3a3-95b2194aa8b9" { + description ""; + source @sa_control_port; + target @sa_repair_request_out; +} + +mission_involvement sa_mission_participant "Mission involvement: Maintain safe water supply" id "1c1e5634-ba8c-591f-b143-90a27cdc6cd2" { + description ""; + source @sa_control; + target @sa_mission; +} + +allocation sa_reset_allocation "Allocation: Reset valve remotely" id "3cc9ab52-2b00-57fd-8709-aabc75e5d3e3" { + description ""; + source @sa_control; + target @sa_reset; +} + +allocation sa_repair_allocation "Allocation: Repair valve in the field" id "725f2e1b-aa2c-5615-9b1f-28da178168fa" { + description ""; + source @sa_technician; + target @sa_repair; +} + +port_allocation sa_port_report_out "Port allocation: Report repair result output" id "e53b7a40-e8a3-5b3f-826d-c2b5c164a84a" { + description ""; + source @sa_technician_port; + target @sa_repair_report_out; +} + +port_allocation sa_port_request_in "Port allocation: Request field repair input" id "09eaa992-4cdf-5ea3-b748-f10fd74b8cba" { + description ""; + source @sa_technician_port; + target @sa_repair_request_in; +} + +interface_provision sa_provider_provides "Interface provision: Valve repair service" id "55f89fbf-aa3c-5ae1-95ef-ac676f9c1ada" { + description ""; + source @sa_technician_port; + target @sa_repair_interface; +} + diff --git a/examples/release1/sa/types.arcadia b/examples/release1/sa/types.arcadia new file mode 100644 index 00000000..faa0117f --- /dev/null +++ b/examples/release1/sa/types.arcadia @@ -0,0 +1,50 @@ +sa_data_type sa_pressure "Measured pressure" id "87d0a95b-af1b-5352-b753-e4c6533671a5" { + default none; + definition { + Real { + maximum 600.0; + minimum 0.0; + }; + }; + description ""; + parent none; + unit "kPa"; +} + +sa_interface sa_repair_interface "Valve repair service" id "a4b8942e-a975-5f88-ad14-8f38c345baac" { + description ""; + parent none; + supertype none; +} + +sa_exchange_item sa_repair_operation "Valve repair" id "a962fc5e-3ab0-5296-8b0d-fd84633f9218" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; +} + +sa_data_field sa_pressure_parameter "Pressure before / after repair" id "bb12ba5d-aab5-5a1b-99de-c767f5365b57" { + default none; + description ""; + direction InOut; + maximum 1; + minimum 1; + parent @sa_repair_operation; + storage Value; + type_ref { + Named @sa_pressure; + }; + unit ""; +} + +interface_item_allocation sa_repair_interface_item "Repair operation contract" id "f074e041-23cf-54ad-af9c-f171e30621e7" { + acquisition Accept; + description ""; + order 0; + source @sa_repair_interface; + target @sa_repair_operation; + transmission Synchronous; +} + diff --git a/examples/release1/traceability.arcadia b/examples/release1/traceability.arcadia new file mode 100644 index 00000000..b185b027 --- /dev/null +++ b/examples/release1/traceability.arcadia @@ -0,0 +1,1156 @@ +trace trace_oa_alarm_transition "State transition: Awaiting repair ↔ State transition: Awaiting repair" id "0c7c8a06-163e-5b3e-8a98-e66e2ce8963b" { + baseline { + oa { + description ""; + name "State transition: Awaiting repair"; + }; + sa { + description ""; + name "State transition: Awaiting repair"; + }; + }; + description ""; + kind Manual; + oa @oa_alarm_transition; + sa @sa_alarm_transition; +} + +trace trace_oa_assess "Assess pressure alarm ↔ Assess pressure alarm" id "5309f955-53fd-5ff4-9c0a-91737e947341" { + baseline { + oa { + description ""; + name "Assess pressure alarm"; + }; + sa { + description ""; + name "Assess pressure alarm"; + }; + }; + description ""; + kind Refinement; + oa @oa_assess; + sa @sa_assess; +} + +trace trace_oa_assess_allocation "Allocation: Assess pressure alarm ↔ Allocation: Assess pressure alarm" id "acdbee9c-5017-5154-804e-50801f0376ec" { + baseline { + oa { + description ""; + name "Allocation: Assess pressure alarm"; + }; + sa { + description ""; + name "Allocation: Assess pressure alarm"; + }; + }; + description ""; + kind Manual; + oa @oa_assess_allocation; + sa @sa_assess_allocation; +} + +trace trace_oa_assess_on_entry "Assess when repair is needed ↔ Assess when repair is needed" id "fb02420d-b066-5773-87c1-198a056e0731" { + baseline { + oa { + description ""; + name "Assess when repair is needed"; + }; + sa { + description ""; + name "Assess when repair is needed"; + }; + }; + description ""; + kind Manual; + oa @oa_assess_on_entry; + sa @sa_assess_on_entry; +} + +trace trace_oa_assess_step "Assess the alarm ↔ Assess the alarm" id "e848da41-445e-5e3c-8b4e-ef53fdddf1e1" { + baseline { + oa { + description ""; + name "Assess the alarm"; + }; + sa { + description ""; + name "Assess the alarm"; + }; + }; + description ""; + kind Sequence; + oa @oa_assess_step; + sa @sa_assess_step; +} + +trace trace_oa_assess_then_decide "Continue ↔ Continue" id "a5c1eb01-a95f-5f7a-9742-7f201493d7aa" { + baseline { + oa { + description ""; + name "Continue"; + }; + sa { + description ""; + name "Continue"; + }; + }; + description ""; + kind Manual; + oa @oa_assess_then_decide; + sa @sa_assess_then_decide; +} + +trace trace_oa_assessment_action "Assess pressure alarm ↔ Assess pressure alarm" id "3a33de40-20a3-5252-899c-c12dc35e9783" { + baseline { + oa { + description ""; + name "Assess pressure alarm"; + }; + sa { + description ""; + name "Assess pressure alarm"; + }; + }; + description ""; + kind Sequence; + oa @oa_assessment_action; + sa @sa_assessment_action; +} + +trace trace_oa_assessment_delivery "Chain involvement: Assess an alarm ↔ Chain involvement: Assess an alarm" id "5b96bfed-63e2-5d7f-b882-91e8b81f8efe" { + baseline { + oa { + description ""; + name "Chain involvement: Assess an alarm"; + }; + sa { + description ""; + name "Chain involvement: Assess an alarm"; + }; + }; + description ""; + kind Manual; + oa @oa_assessment_delivery; + sa @sa_assessment_delivery; +} + +trace trace_oa_assessment_procedure "Assess an alarm procedure ↔ Assess an alarm procedure" id "6ca4c78a-a5f2-5470-b401-732f5a8825d0" { + baseline { + oa { + description ""; + name "Assess an alarm procedure"; + }; + sa { + description ""; + name "Assess an alarm procedure"; + }; + }; + description ""; + kind Sequence; + oa @oa_assessment_procedure; + sa @sa_assessment_procedure; +} + +trace trace_oa_assessment_support "Capability involvement: Assess an alarm ↔ Capability involvement: Assess an alarm" id "6e13cf86-7242-5e33-8912-b037cf6e8c87" { + baseline { + oa { + description ""; + name "Capability involvement: Assess an alarm"; + }; + sa { + description ""; + name "Capability involvement: Assess an alarm"; + }; + }; + description ""; + kind Manual; + oa @oa_assessment_support; + sa @sa_assessment_support; +} + +trace trace_oa_call "Request valve repair ↔ Request valve repair" id "f0cc86e5-f8fd-5091-a40d-dbfb6515072f" { + baseline { + oa { + description ""; + name "Request valve repair"; + }; + sa { + description ""; + name "Request valve repair"; + }; + }; + description ""; + kind Scenario; + oa @oa_call; + sa @sa_call; +} + +trace trace_oa_caller "Controller ↔ Controller" id "241dfdac-2450-5704-a4cb-9f1047db1b6d" { + baseline { + oa { + description ""; + name "Controller"; + }; + sa { + description ""; + name "Controller"; + }; + }; + description ""; + kind Scenario; + oa @oa_caller; + sa @sa_caller; +} + +trace trace_oa_confirm "Confirm safe pressure ↔ Confirm safe pressure" id "d7773ff5-cef5-50c1-8f9d-c5659115e1f0" { + baseline { + oa { + description ""; + name "Confirm safe pressure"; + }; + sa { + description ""; + name "Confirm safe pressure"; + }; + }; + description ""; + kind Refinement; + oa @oa_confirm; + sa @sa_confirm; +} + +trace trace_oa_confirm_allocation "Allocation: Confirm safe pressure ↔ Allocation: Confirm safe pressure" id "2fdcbc6f-526e-5a4e-bde5-314cf0cf3ef3" { + baseline { + oa { + description ""; + name "Allocation: Confirm safe pressure"; + }; + sa { + description ""; + name "Allocation: Confirm safe pressure"; + }; + }; + description ""; + kind Manual; + oa @oa_confirm_allocation; + sa @sa_confirm_allocation; +} + +trace trace_oa_confirm_path "Continue ↔ Continue" id "a106d183-c239-5c9f-b1f1-141af19fc778" { + baseline { + oa { + description ""; + name "Continue"; + }; + sa { + description ""; + name "Continue"; + }; + }; + description ""; + kind Manual; + oa @oa_confirm_path; + sa @sa_confirm_path; +} + +trace trace_oa_confirm_step "Confirm safe pressure ↔ Confirm safe pressure" id "154abfce-c311-52d1-85d1-3f4c85203b26" { + baseline { + oa { + description ""; + name "Confirm safe pressure"; + }; + sa { + description ""; + name "Confirm safe pressure"; + }; + }; + description ""; + kind Sequence; + oa @oa_confirm_step; + sa @sa_confirm_step; +} + +trace trace_oa_contact "Technician service conversation ↔ Technician service conversation" id "c9d65349-3400-58eb-8a55-57fa02de4b67" { + baseline { + oa { + description ""; + name "Technician service conversation"; + }; + sa { + description ""; + name "Technician service conversation"; + }; + }; + description ""; + kind Exchange; + oa @oa_contact; + sa @sa_contact; +} + +trace trace_oa_contact_category "Field contact ↔ Field contact" id "e96ff119-bb4d-5fd9-9ab7-2ede0d3fd418" { + baseline { + oa { + description ""; + name "Field contact"; + }; + sa { + description ""; + name "Field contact"; + }; + }; + description ""; + kind Manual; + oa @oa_contact_category; + sa @sa_contact_category; +} + +trace trace_oa_control "Control room ↔ Pressure coordination service" id "aba528ea-e396-582a-bfba-f6e2212d391a" { + baseline { + oa { + description ""; + name "Control room"; + }; + sa { + description ""; + name "Pressure coordination service"; + }; + }; + description ""; + kind Participant; + oa @oa_control; + sa @sa_control; +} + +trace trace_oa_conversation "Repair request and measured reply ↔ Repair request and measured reply" id "3694c752-ba80-5702-a7d7-9cc66b6b01d8" { + baseline { + oa { + description ""; + name "Repair request and measured reply"; + }; + sa { + description ""; + name "Repair request and measured reply"; + }; + }; + description ""; + kind Scenario; + oa @oa_conversation; + sa @sa_conversation; +} + +trace trace_oa_decision "Can a remote reset safely recover pressure? ↔ Can a remote reset safely recover pressure?" id "bbf6f1a1-cb2b-5632-8ec0-bf4b8ef1a941" { + baseline { + oa { + description ""; + name "Can a remote reset safely recover pressure?"; + }; + sa { + description ""; + name "Can a remote reset safely recover pressure?"; + }; + }; + description ""; + kind Sequence; + oa @oa_decision; + sa @sa_decision; +} + +trace trace_oa_field_merge "Continue ↔ Continue" id "d32dd433-9c9e-5b57-9355-2970cc44a5f2" { + baseline { + oa { + description ""; + name "Continue"; + }; + sa { + description ""; + name "Continue"; + }; + }; + description ""; + kind Manual; + oa @oa_field_merge; + sa @sa_field_merge; +} + +trace trace_oa_field_path "else ↔ else" id "2e849e31-35aa-5aef-a989-26948038e52b" { + baseline { + oa { + description ""; + name "else"; + }; + sa { + description ""; + name "else"; + }; + }; + description ""; + kind Manual; + oa @oa_field_path; + sa @sa_field_path; +} + +trace trace_oa_field_procedure "Field repair procedure ↔ Field repair procedure" id "bb72b2c2-6e32-5f65-9d8e-b33c1a65da37" { + baseline { + oa { + description ""; + name "Field repair procedure"; + }; + sa { + description ""; + name "Field repair procedure"; + }; + }; + description ""; + kind Sequence; + oa @oa_field_procedure; + sa @sa_field_procedure; +} + +trace trace_oa_full_scenario "Restore supply through field repair ↔ Restore supply through field repair" id "435bae4c-cf6d-59f3-9eb5-d660ebeec079" { + baseline { + oa { + description ""; + name "Restore supply through field repair"; + }; + sa { + description ""; + name "Restore supply through field repair"; + }; + }; + description ""; + kind Scenario; + oa @oa_full_scenario; + sa @sa_full_scenario; +} + +trace trace_oa_high_pressure "Pressure alarm detected ↔ Pressure alarm detected" id "b97749d4-f9ad-5fb7-9d78-bc60a74e98eb" { + baseline { + oa { + description ""; + name "Pressure alarm detected"; + }; + sa { + description ""; + name "Pressure alarm detected"; + }; + }; + description ""; + kind State; + oa @oa_high_pressure; + sa @sa_high_pressure; +} + +trace trace_oa_include_triage "Capability include: Assess an alarm ↔ Capability include: Assess an alarm" id "77073fcb-53aa-5f06-987a-0317cbb6971d" { + baseline { + oa { + description ""; + name "Capability include: Assess an alarm"; + }; + sa { + description ""; + name "Capability include: Assess an alarm"; + }; + }; + description ""; + kind Manual; + oa @oa_include_triage; + sa @sa_include_triage; +} + +trace trace_oa_merge "Recovery path completed ↔ Recovery path completed" id "0c9de959-d366-5765-98eb-450ea27bd316" { + baseline { + oa { + description ""; + name "Recovery path completed"; + }; + sa { + description ""; + name "Recovery path completed"; + }; + }; + description ""; + kind Sequence; + oa @oa_merge; + sa @sa_merge; +} + +trace trace_oa_mission "Maintain safe water supply ↔ Maintain safe water supply" id "c987bd0f-87f6-5550-945d-2a42450fe1c5" { + baseline { + oa { + description ""; + name "Maintain safe water supply"; + }; + sa { + description ""; + name "Maintain safe water supply"; + }; + }; + description ""; + kind Mission; + oa @oa_mission; + sa @sa_mission; +} + +trace trace_oa_mission_delivery "Mission capability: Restore a safe supply ↔ Mission capability: Restore a safe supply" id "aa164208-394d-5bc7-8462-b0043f682aad" { + baseline { + oa { + description ""; + name "Mission capability: Restore a safe supply"; + }; + sa { + description ""; + name "Mission capability: Restore a safe supply"; + }; + }; + description ""; + kind Manual; + oa @oa_mission_delivery; + sa @sa_mission_delivery; +} + +trace trace_oa_mission_participant "Mission involvement: Maintain safe water supply ↔ Mission involvement: Maintain safe water supply" id "8d693d90-5017-56af-b056-d29d3cb46baa" { + baseline { + oa { + description ""; + name "Mission involvement: Maintain safe water supply"; + }; + sa { + description ""; + name "Mission involvement: Maintain safe water supply"; + }; + }; + description ""; + kind Manual; + oa @oa_mission_participant; + sa @sa_mission_participant; +} + +trace trace_oa_monitor "Pressure supervision ↔ Pressure supervision" id "b53863a2-ed80-5f98-ad3c-50e1b821066d" { + baseline { + oa { + description ""; + name "Pressure supervision"; + }; + sa { + description ""; + name "Pressure supervision"; + }; + }; + description ""; + kind State; + oa @oa_monitor; + sa @sa_monitor; +} + +trace trace_oa_monitor_initial "Start ↔ Start" id "48d243f0-553f-507e-acb0-9474ae156b63" { + baseline { + oa { + description ""; + name "Start"; + }; + sa { + description ""; + name "Start"; + }; + }; + description ""; + kind State; + oa @oa_monitor_initial; + sa @sa_monitor_initial; +} + +trace trace_oa_monitor_region "Supervision cycle ↔ Supervision cycle" id "208753e0-9151-50d8-86ca-29fc28eca80b" { + baseline { + oa { + description ""; + name "Supervision cycle"; + }; + sa { + description ""; + name "Supervision cycle"; + }; + }; + description ""; + kind State; + oa @oa_monitor_region; + sa @sa_monitor_region; +} + +trace trace_oa_monitoring "Monitoring ↔ Monitoring" id "12ec320f-2e14-5a5a-8c6b-bd2b6471dae9" { + baseline { + oa { + description ""; + name "Monitoring"; + }; + sa { + description ""; + name "Monitoring"; + }; + }; + description ""; + kind State; + oa @oa_monitoring; + sa @sa_monitoring; +} + +trace trace_oa_normal_transition "State transition: Monitoring ↔ State transition: Monitoring" id "c47cf0b4-fe15-5175-bd62-5292e43adc39" { + baseline { + oa { + description ""; + name "State transition: Monitoring"; + }; + sa { + description ""; + name "State transition: Monitoring"; + }; + }; + description ""; + kind Manual; + oa @oa_normal_transition; + sa @sa_normal_transition; +} + +trace trace_oa_outer_controller "Controller ↔ Controller" id "fd195498-dbba-523b-94d0-49f3b9531a3e" { + baseline { + oa { + description ""; + name "Controller"; + }; + sa { + description ""; + name "Controller"; + }; + }; + description ""; + kind Scenario; + oa @oa_outer_controller; + sa @sa_outer_controller; +} + +trace trace_oa_outer_technician "Technician ↔ Technician" id "c132a0df-1270-55a8-ac4e-6e5075823dc1" { + baseline { + oa { + description ""; + name "Technician"; + }; + sa { + description ""; + name "Technician"; + }; + }; + description ""; + kind Scenario; + oa @oa_outer_technician; + sa @sa_outer_technician; +} + +trace trace_oa_pressure "Measured pressure ↔ Measured pressure" id "d9d47371-e98d-5d29-9df7-4de2d28a5c73" { + baseline { + oa { + description ""; + name "Measured pressure"; + }; + sa { + description ""; + name "Measured pressure"; + }; + }; + description ""; + kind Item; + oa @oa_pressure; + sa @sa_pressure; +} + +trace trace_oa_pressure_parameter "Pressure before / after repair ↔ Pressure before / after repair" id "0d905fbb-5b3c-56f0-9f11-a27091901eb0" { + baseline { + oa { + description ""; + name "Pressure before / after repair"; + }; + sa { + description ""; + name "Pressure before / after repair"; + }; + }; + description ""; + kind Item; + oa @oa_pressure_parameter; + sa @sa_pressure_parameter; +} + +trace trace_oa_remote_merge "Continue ↔ Continue" id "1b788e74-36b2-5c17-a936-642dbe51ca3c" { + baseline { + oa { + description ""; + name "Continue"; + }; + sa { + description ""; + name "Continue"; + }; + }; + description ""; + kind Manual; + oa @oa_remote_merge; + sa @sa_remote_merge; +} + +trace trace_oa_remote_path "remote reset is authorized ↔ remote reset is authorized" id "eee79ec1-41d7-5374-986e-995a56b9026f" { + baseline { + oa { + description ""; + name "remote reset is authorized"; + }; + sa { + description ""; + name "remote reset is authorized"; + }; + }; + description ""; + kind Manual; + oa @oa_remote_path; + sa @sa_remote_path; +} + +trace trace_oa_repair "Repair valve in the field ↔ Repair valve in the field" id "b13452bb-1a55-5ad6-b6d9-a37fb34e8d13" { + baseline { + oa { + description ""; + name "Repair valve in the field"; + }; + sa { + description ""; + name "Repair valve in the field"; + }; + }; + description ""; + kind Refinement; + oa @oa_repair; + sa @sa_repair; +} + +trace trace_oa_repair_allocation "Allocation: Repair valve in the field ↔ Allocation: Repair valve in the field" id "3714d547-006f-5c3a-97f8-175ccc23d994" { + baseline { + oa { + description ""; + name "Allocation: Repair valve in the field"; + }; + sa { + description ""; + name "Allocation: Repair valve in the field"; + }; + }; + description ""; + kind Manual; + oa @oa_repair_allocation; + sa @sa_repair_allocation; +} + +trace trace_oa_repair_operation "Valve repair ↔ Valve repair" id "cf92cedf-ea1a-510c-9bc5-89c16d60e8d2" { + baseline { + oa { + description ""; + name "Valve repair"; + }; + sa { + description ""; + name "Valve repair"; + }; + }; + description ""; + kind Item; + oa @oa_repair_operation; + sa @sa_repair_operation; +} + +trace trace_oa_repair_report "Report repair result ↔ Report repair result" id "424eca39-1c29-5a2f-b9ee-33dd018926aa" { + baseline { + oa { + description ""; + name "Report repair result"; + }; + sa { + description ""; + name "Report repair result"; + }; + }; + description ""; + kind Exchange; + oa @oa_repair_report; + sa @sa_repair_report; +} + +trace trace_oa_repair_request "Request field repair ↔ Request field repair" id "da8efc3c-78df-5b10-b2e5-1e28d6d99e86" { + baseline { + oa { + description ""; + name "Request field repair"; + }; + sa { + description ""; + name "Request field repair"; + }; + }; + description ""; + kind Exchange; + oa @oa_repair_request; + sa @sa_repair_request; +} + +trace trace_oa_repair_step "Repair the valve ↔ Repair the valve" id "ac3aa6b0-568e-5a3f-963a-172aa3cfc7a4" { + baseline { + oa { + description ""; + name "Repair the valve"; + }; + sa { + description ""; + name "Repair the valve"; + }; + }; + description ""; + kind Sequence; + oa @oa_repair_step; + sa @sa_repair_step; +} + +trace trace_oa_repair_then_report "Sequence link: Report the measured result ↔ Sequence link: Report the measured result" id "f8d34715-1dc8-504b-acdf-78b770aad1e8" { + baseline { + oa { + description ""; + name "Sequence link: Report the measured result"; + }; + sa { + description ""; + name "Sequence link: Report the measured result"; + }; + }; + description ""; + kind Manual; + oa @oa_repair_then_report; + sa @sa_repair_then_report; +} + +trace trace_oa_repair_traffic "Repair work exchanges ↔ Repair work exchanges" id "3e7bde54-f9c1-549f-9e5b-2ce1ecb685a7" { + baseline { + oa { + description ""; + name "Repair work exchanges"; + }; + sa { + description ""; + name "Repair work exchanges"; + }; + }; + description ""; + kind Manual; + oa @oa_repair_traffic; + sa @sa_repair_traffic; +} + +trace trace_oa_reply "Return pressure after repair ↔ Return pressure after repair" id "595b5729-ccd9-5291-b5d9-30d8c8bd9dfa" { + baseline { + oa { + description ""; + name "Return pressure after repair"; + }; + sa { + description ""; + name "Return pressure after repair"; + }; + }; + description ""; + kind Scenario; + oa @oa_reply; + sa @sa_reply; +} + +trace trace_oa_report_step "Report the measured result ↔ Report the measured result" id "e63b14d2-f408-56a2-aec4-be20cf4ace73" { + baseline { + oa { + description ""; + name "Report the measured result"; + }; + sa { + description ""; + name "Report the measured result"; + }; + }; + description ""; + kind Sequence; + oa @oa_report_step; + sa @sa_report_step; +} + +trace trace_oa_request_step "Request the field repair ↔ Request the field repair" id "ad697c00-07d9-5799-89b3-2c78da8dff4d" { + baseline { + oa { + description ""; + name "Request the field repair"; + }; + sa { + description ""; + name "Request the field repair"; + }; + }; + description ""; + kind Sequence; + oa @oa_request_step; + sa @sa_request_step; +} + +trace trace_oa_request_then_repair "Sequence link: Repair the valve ↔ Sequence link: Repair the valve" id "f2b7050c-629d-533f-b873-bffc33e072f7" { + baseline { + oa { + description ""; + name "Sequence link: Repair the valve"; + }; + sa { + description ""; + name "Sequence link: Repair the valve"; + }; + }; + description ""; + kind Manual; + oa @oa_request_then_repair; + sa @sa_request_then_repair; +} + +trace trace_oa_reset "Reset valve remotely ↔ Reset valve remotely" id "82a5be1a-92a8-578d-9448-086062a20d8f" { + baseline { + oa { + description ""; + name "Reset valve remotely"; + }; + sa { + description ""; + name "Reset valve remotely"; + }; + }; + description ""; + kind Refinement; + oa @oa_reset; + sa @sa_reset; +} + +trace trace_oa_reset_allocation "Allocation: Reset valve remotely ↔ Allocation: Reset valve remotely" id "98ef05b5-5381-5d35-a433-ebab55e9f382" { + baseline { + oa { + description ""; + name "Allocation: Reset valve remotely"; + }; + sa { + description ""; + name "Allocation: Reset valve remotely"; + }; + }; + description ""; + kind Manual; + oa @oa_reset_allocation; + sa @sa_reset_allocation; +} + +trace trace_oa_reset_step "Try the authorized remote reset ↔ Try the authorized remote reset" id "ff84fc06-6f99-5cf3-b73d-236e9eff98a3" { + baseline { + oa { + description ""; + name "Try the authorized remote reset"; + }; + sa { + description ""; + name "Try the authorized remote reset"; + }; + }; + description ""; + kind Sequence; + oa @oa_reset_step; + sa @sa_reset_step; +} + +trace trace_oa_responder "Technician ↔ Technician" id "c5051628-b7af-5fb1-a014-bb60a0b46e03" { + baseline { + oa { + description ""; + name "Technician"; + }; + sa { + description ""; + name "Technician"; + }; + }; + description ""; + kind Scenario; + oa @oa_responder; + sa @sa_responder; +} + +trace trace_oa_response "Handle a pressure alarm ↔ Handle a pressure alarm" id "d3626818-4846-5893-9c02-3407cdba7f3c" { + baseline { + oa { + description ""; + name "Handle a pressure alarm"; + }; + sa { + description ""; + name "Handle a pressure alarm"; + }; + }; + description ""; + kind Sequence; + oa @oa_response; + sa @sa_response; +} + +trace trace_oa_response_delivery "Chain involvement: Restore a safe supply ↔ Chain involvement: Restore a safe supply" id "70c90b0c-85ce-574a-a217-f0d19fad30e1" { + baseline { + oa { + description ""; + name "Chain involvement: Restore a safe supply"; + }; + sa { + description ""; + name "Chain involvement: Restore a safe supply"; + }; + }; + description ""; + kind Manual; + oa @oa_response_delivery; + sa @sa_response_delivery; +} + +trace trace_oa_restore "Restore a safe supply ↔ Restore a safe supply" id "0fbef164-b5d3-58ba-ba41-54d0e63709ab" { + baseline { + oa { + description ""; + name "Restore a safe supply"; + }; + sa { + description ""; + name "Restore a safe supply"; + }; + }; + description ""; + kind Capability; + oa @oa_restore; + sa @sa_restore; +} + +trace trace_oa_restore_work "Restore safe pressure ↔ Restore safe pressure" id "82cca563-9d5d-5479-aa1a-9db03e9674d1" { + baseline { + oa { + description ""; + name "Restore safe pressure"; + }; + sa { + description ""; + name "Restore safe pressure"; + }; + }; + description ""; + kind Refinement; + oa @oa_restore_work; + sa @sa_restore_work; +} + +trace trace_oa_start_monitoring "State transition: Monitoring ↔ State transition: Monitoring" id "07a7b82a-b5a7-5737-abfa-09e422f7420e" { + baseline { + oa { + description ""; + name "State transition: Monitoring"; + }; + sa { + description ""; + name "State transition: Monitoring"; + }; + }; + description ""; + kind Manual; + oa @oa_start_monitoring; + sa @sa_start_monitoring; +} + +trace trace_oa_technician "Field technician ↔ Field technician" id "d47d3c36-50fb-5189-b801-d6fd1694bd49" { + baseline { + oa { + description ""; + name "Field technician"; + }; + sa { + description ""; + name "Field technician"; + }; + }; + description ""; + kind Participant; + oa @oa_technician; + sa @sa_technician; +} + +trace trace_oa_triage "Assess an alarm ↔ Assess an alarm" id "6acd072d-431e-5881-ab20-1813b4fda0c5" { + baseline { + oa { + description ""; + name "Assess an alarm"; + }; + sa { + description ""; + name "Assess an alarm"; + }; + }; + description ""; + kind Capability; + oa @oa_triage; + sa @sa_triage; +} + +trace trace_oa_use_conversation "Use the reviewed repair conversation ↔ Use the reviewed repair conversation" id "f5733d3f-eee3-5cf7-a422-5edd2054d6a6" { + baseline { + oa { + description ""; + name "Use the reviewed repair conversation"; + }; + sa { + description ""; + name "Use the reviewed repair conversation"; + }; + }; + description ""; + kind Scenario; + oa @oa_use_conversation; + sa @sa_use_conversation; +} + +trace trace_oa_use_field "Use the field repair procedure ↔ Use the field repair procedure" id "7a6b4b9c-4220-546a-a752-1587c2881fe1" { + baseline { + oa { + description ""; + name "Use the field repair procedure"; + }; + sa { + description ""; + name "Use the field repair procedure"; + }; + }; + description ""; + kind Sequence; + oa @oa_use_field; + sa @sa_use_field; +} + +trace trace_oa_waiting "Awaiting repair ↔ Awaiting repair" id "0754be23-e4b8-53bc-b03c-89c2dbd80361" { + baseline { + oa { + description ""; + name "Awaiting repair"; + }; + sa { + description ""; + name "Awaiting repair"; + }; + }; + description ""; + kind State; + oa @oa_waiting; + sa @sa_waiting; +} + diff --git a/examples/release1/transition-before.json b/examples/release1/transition-before.json new file mode 100644 index 00000000..9597fb17 --- /dev/null +++ b/examples/release1/transition-before.json @@ -0,0 +1,1198 @@ +{ + "alias": "pressure_restoration", + "artifact_links": {}, + "description": "A controller either resets a valve remotely or requests field repair. Compare the operational need with the explicitly chosen coordination-service boundary. Start with README.md beside project.arcadia. Units and guards are descriptive; this model does not simulate a real water network.", + "diagrams": {}, + "elements": { + "022d3cfa-bbaf-5e13-924a-8e55099b8745": { + "alias": "oa_assessment_procedure", + "body": { + "Oa": { + "Process": { + "steps": [ + "942975b8-0bef-5ae8-9fcb-c37497833fb0" + ] + } + } + }, + "description": "", + "id": "022d3cfa-bbaf-5e13-924a-8e55099b8745", + "name": "Assess an alarm procedure", + "parent": null + }, + "0b07b9ed-492d-57f6-b0b1-ce8a709238a7": { + "alias": "oa_assess_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "SequenceReference": { + "sequence": "022d3cfa-bbaf-5e13-924a-8e55099b8745" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "name": "Assess the alarm", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "1970608f-85d1-5234-ac86-5e2b0df27dc3": { + "alias": "oa_monitor", + "body": { + "Oa": { + "Shared": "StateMachine" + } + }, + "description": "", + "id": "1970608f-85d1-5234-ac86-5e2b0df27dc3", + "name": "Pressure supervision", + "parent": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" + }, + "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771": { + "alias": "oa_contact_category", + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "exchanges": [ + "27b47887-baf0-518b-bf87-f5acbf7d0b93" + ], + "family": "Participant" + } + } + } + }, + "description": "", + "id": "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771", + "name": "Field contact", + "parent": null + }, + "1b91f8a7-0bdb-514e-b1cd-955658e09927": { + "alias": "oa_restore_work", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "1b91f8a7-0bdb-514e-b1cd-955658e09927", + "name": "Restore safe pressure", + "parent": null + }, + "2011b3ac-614b-5a47-8891-7ff81965f425": { + "alias": "oa_outer_controller", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" + } + } + } + }, + "description": "", + "id": "2011b3ac-614b-5a47-8891-7ff81965f425", + "name": "Controller", + "parent": "d6f881a4-95da-54e9-ad97-1b8965532629" + }, + "20472233-5ce6-5ffe-b4ad-0bdbccf3e423": { + "alias": "oa_conversation", + "body": { + "Oa": { + "Shared": { + "Scenario": { + "context": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "postcondition": "The controller has received the measured pressure after repair", + "precondition": "A pressure alarm has been assessed", + "steps": [ + "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "cc2d497a-fd15-5455-9ee5-e70eb95811d8" + ], + "viewpoint": "ParticipantInteraction" + } + } + } + }, + "description": "", + "id": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423", + "name": "Repair request and measured reply", + "parent": null + }, + "32aa9940-df6e-5a12-b9bd-6337e2d2ed22": { + "alias": "oa_control", + "body": { + "Oa": "Entity" + }, + "description": "", + "id": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "name": "Control room", + "parent": null + }, + "3dfee23a-148b-5cec-ad2d-b774e84ea5bb": { + "alias": "oa_restore", + "body": { + "Oa": "Capability" + }, + "description": "", + "id": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "name": "Restore a safe supply", + "parent": null + }, + "41859a65-ded2-547b-ab03-14e2fa740cf9": { + "alias": "oa_caller", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22" + } + } + } + }, + "description": "", + "id": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "name": "Controller", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "435bf87c-73fd-5e4d-978a-4c471bc9eaf0": { + "alias": "sa_control", + "body": { + "Sa": "System" + }, + "description": "", + "id": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "name": "Pressure coordination service", + "parent": null + }, + "58bae2b0-f560-50f2-818d-19091d22d18a": { + "alias": "oa_response", + "body": { + "Oa": { + "Process": { + "steps": [ + "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "e00594a3-6122-5a6b-b3c2-ecb5506c741d" + ] + } + } + }, + "description": "", + "id": "58bae2b0-f560-50f2-818d-19091d22d18a", + "name": "Handle a pressure alarm", + "parent": null + }, + "58c15833-6e75-523a-b57f-a88f19f6d672": { + "alias": "oa_waiting", + "body": { + "Oa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "State" + } + } + } + }, + "description": "", + "id": "58c15833-6e75-523a-b57f-a88f19f6d672", + "name": "Awaiting repair", + "parent": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" + }, + "5ba8079c-2dbe-5e98-ae82-2b60c6020a02": { + "alias": "oa_request_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "message_kind": "Asynchronous", + "receiver": null, + "reply_to": null, + "sender": null, + "values": [] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "name": "Request the field repair", + "parent": "5d3af417-244c-54bb-9588-16daedda4e1f" + }, + "5d3af417-244c-54bb-9588-16daedda4e1f": { + "alias": "oa_field_procedure", + "body": { + "Oa": { + "Process": { + "steps": [ + "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "c59432fe-6092-5ee0-908d-9dbf39476471", + "df2fd3f8-6bc2-5380-a13f-36643ed35ac7" + ] + } + } + }, + "description": "", + "id": "5d3af417-244c-54bb-9588-16daedda4e1f", + "name": "Field repair procedure", + "parent": null + }, + "61f7bbfb-74a4-5540-9182-2583dfb27a49": { + "alias": "oa_assess", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "name": "Assess pressure alarm", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "65d6b8d6-271a-5c84-8b42-abbe310c955e": { + "alias": "oa_high_pressure", + "body": { + "Oa": { + "Shared": { + "Event": { + "body": { + "Change": { + "expression": "measured_pressure > operating_limit" + } + } + } + } + } + }, + "description": "", + "id": "65d6b8d6-271a-5c84-8b42-abbe310c955e", + "name": "Pressure alarm detected", + "parent": null + }, + "6a30b763-cc51-546d-907f-5c63657cfd03": { + "alias": "oa_triage", + "body": { + "Oa": "Capability" + }, + "description": "", + "id": "6a30b763-cc51-546d-907f-5c63657cfd03", + "name": "Assess an alarm", + "parent": null + }, + "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d": { + "alias": "oa_merge", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Control": { + "kind": "Merge" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "name": "Recovery path completed", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae": { + "alias": "oa_monitoring", + "body": { + "Oa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "State" + } + } + } + }, + "description": "", + "id": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "name": "Monitoring", + "parent": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" + }, + "727c0b5c-8db6-5eb7-83d6-735519c5949f": { + "alias": "oa_confirm", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "name": "Confirm safe pressure", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7": { + "alias": "oa_monitor_region", + "body": { + "Oa": { + "Shared": "Region" + } + }, + "description": "", + "id": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7", + "name": "Supervision cycle", + "parent": "1970608f-85d1-5234-ac86-5e2b0df27dc3" + }, + "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7": { + "alias": "oa_pressure_parameter", + "body": { + "Oa": { + "Shared": { + "DataField": { + "default": null, + "direction": "InOut", + "maximum": 1, + "minimum": 1, + "storage": "Value", + "type_ref": { + "Named": "e7824764-5db9-5384-8198-2fbaea2b10b2" + }, + "unit": "" + } + } + } + }, + "description": "", + "id": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "name": "Pressure before / after repair", + "parent": "d736c7d6-d369-550a-8ba4-dae223e40bb3" + }, + "9349b353-53f2-56f8-b571-a05b09335cdf": { + "alias": "oa_repair_traffic", + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "exchanges": [ + "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "b436d0b3-c824-5a96-af6b-2c269816d1cc" + ], + "family": "Work" + } + } + } + }, + "description": "", + "id": "9349b353-53f2-56f8-b571-a05b09335cdf", + "name": "Repair work exchanges", + "parent": null + }, + "942975b8-0bef-5ae8-9fcb-c37497833fb0": { + "alias": "oa_assessment_action", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "942975b8-0bef-5ae8-9fcb-c37497833fb0", + "name": "Assess pressure alarm", + "parent": "022d3cfa-bbaf-5e13-924a-8e55099b8745" + }, + "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5": { + "alias": "oa_use_conversation", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "ScenarioUse": { + "bindings": [ + { + "local": "2011b3ac-614b-5a47-8891-7ff81965f425", + "referenced": "41859a65-ded2-547b-ab03-14e2fa740cf9" + }, + { + "local": "fffff882-78fd-5415-928d-c1ef7fac6576", + "referenced": "fc81c5f0-0442-5b88-992b-e5b10f351c59" + } + ], + "scenario": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5", + "name": "Use the reviewed repair conversation", + "parent": "d6f881a4-95da-54e9-ad97-1b8965532629" + }, + "a21465af-05f3-5fb5-8983-de26932422fb": { + "alias": "oa_repair", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "a21465af-05f3-5fb5-8983-de26932422fb", + "name": "Repair valve in the field", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "a7ffb869-9dde-5e76-b276-c719c5c143a9": { + "alias": "oa_monitor_initial", + "body": { + "Oa": { + "Shared": { + "State": { + "do_activity": "", + "entry": "", + "exit": "", + "kind": "Initial" + } + } + } + }, + "description": "", + "id": "a7ffb869-9dde-5e76-b276-c719c5c143a9", + "name": "Start", + "parent": "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7" + }, + "b280c6ca-e5f9-5424-a5c1-5676e01c2371": { + "alias": "oa_decision", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Control": { + "kind": "Choice" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "name": "Can a remote reset safely recover pressure?", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "b97437c6-cd7a-5a05-b537-b30a17dd936a": { + "alias": "oa_call", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "message_kind": "Call", + "receiver": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "reply_to": null, + "sender": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "values": [ + { + "expression": "420.0", + "field": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "path": [] + } + ] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "name": "Request valve repair", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "c05b9422-b9da-5de5-b21d-07ad23bd2b8a": { + "alias": "sa_technician", + "body": { + "Sa": "Actor" + }, + "description": "", + "id": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a", + "name": "Field technician", + "parent": null + }, + "c59432fe-6092-5ee0-908d-9dbf39476471": { + "alias": "oa_repair_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "a21465af-05f3-5fb5-8983-de26932422fb", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "c59432fe-6092-5ee0-908d-9dbf39476471", + "name": "Repair the valve", + "parent": "5d3af417-244c-54bb-9588-16daedda4e1f" + }, + "c9c61f50-06e0-5d17-973d-dcc57cd5cc90": { + "alias": "oa_mission", + "body": { + "Oa": "Mission" + }, + "description": "", + "id": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90", + "name": "Maintain safe water supply", + "parent": null + }, + "cc2d497a-fd15-5455-9ee5-e70eb95811d8": { + "alias": "oa_reply", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "message_kind": "Reply", + "receiver": "41859a65-ded2-547b-ab03-14e2fa740cf9", + "reply_to": "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "sender": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "values": [ + { + "expression": "320.0", + "field": "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "path": [] + } + ] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "cc2d497a-fd15-5455-9ee5-e70eb95811d8", + "name": "Return pressure after repair", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "d0c8be87-d238-503a-a549-7558a210507f": { + "alias": "oa_reset", + "body": { + "Oa": "Activity" + }, + "description": "", + "id": "d0c8be87-d238-503a-a549-7558a210507f", + "name": "Reset valve remotely", + "parent": "1b91f8a7-0bdb-514e-b1cd-955658e09927" + }, + "d2026fc2-74a8-5e22-9942-ff3eb68a3677": { + "alias": "oa_reset_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "d0c8be87-d238-503a-a549-7558a210507f", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "name": "Try the authorized remote reset", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "d6f881a4-95da-54e9-ad97-1b8965532629": { + "alias": "oa_full_scenario", + "body": { + "Oa": { + "Shared": { + "Scenario": { + "context": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "postcondition": "The controller has received the measured pressure after repair", + "precondition": "A pressure alarm has been assessed", + "steps": [ + "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5" + ], + "viewpoint": "ParticipantInteraction" + } + } + } + }, + "description": "", + "id": "d6f881a4-95da-54e9-ad97-1b8965532629", + "name": "Restore supply through field repair", + "parent": null + }, + "d736c7d6-d369-550a-8ba4-dae223e40bb3": { + "alias": "oa_repair_operation", + "body": { + "Oa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + }, + "description": "", + "id": "d736c7d6-d369-550a-8ba4-dae223e40bb3", + "name": "Valve repair", + "parent": null + }, + "df2fd3f8-6bc2-5380-a13f-36643ed35ac7": { + "alias": "oa_report_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "b436d0b3-c824-5a96-af6b-2c269816d1cc", + "message_kind": "Asynchronous", + "receiver": null, + "reply_to": null, + "sender": null, + "values": [] + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "df2fd3f8-6bc2-5380-a13f-36643ed35ac7", + "name": "Report the measured result", + "parent": "5d3af417-244c-54bb-9588-16daedda4e1f" + }, + "e00594a3-6122-5a6b-b3c2-ecb5506c741d": { + "alias": "oa_confirm_step", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Action": { + "action": "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "lifeline": null + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "e00594a3-6122-5a6b-b3c2-ecb5506c741d", + "name": "Confirm safe pressure", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "e07c3952-4a1a-5cc7-b7d5-03a01101ce49": { + "alias": "oa_technician", + "body": { + "Oa": "Actor" + }, + "description": "", + "id": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "name": "Field technician", + "parent": null + }, + "e7824764-5db9-5384-8198-2fbaea2b10b2": { + "alias": "oa_pressure", + "body": { + "Oa": { + "Shared": { + "DataType": { + "default": null, + "definition": { + "Real": { + "maximum": 600.0, + "minimum": 0.0 + } + }, + "unit": "kPa" + } + } + } + }, + "description": "", + "id": "e7824764-5db9-5384-8198-2fbaea2b10b2", + "name": "Measured pressure", + "parent": null + }, + "fc6763bf-a534-54bf-afcd-57ab285ff07d": { + "alias": "oa_use_field", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "SequenceReference": { + "sequence": "5d3af417-244c-54bb-9588-16daedda4e1f" + } + }, + "postcondition": "", + "precondition": "" + } + } + } + }, + "description": "", + "id": "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "name": "Use the field repair procedure", + "parent": "58bae2b0-f560-50f2-818d-19091d22d18a" + }, + "fc81c5f0-0442-5b88-992b-e5b10f351c59": { + "alias": "oa_responder", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" + } + } + } + }, + "description": "", + "id": "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "name": "Technician", + "parent": "20472233-5ce6-5ffe-b4ad-0bdbccf3e423" + }, + "fffff882-78fd-5415-928d-c1ef7fac6576": { + "alias": "oa_outer_technician", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" + } + } + } + }, + "description": "", + "id": "fffff882-78fd-5415-928d-c1ef7fac6576", + "name": "Technician", + "parent": "d6f881a4-95da-54e9-ad97-1b8965532629" + } + }, + "id": "2786d72f-5763-5f33-a57f-2773a4ca1a6c", + "name": "Restore safe water pressure — release 1 walkthrough", + "relations": { + "094b888c-450c-5c4f-971a-7d75db059574": { + "alias": "oa_request_carriage", + "body": { + "ExchangeAllocation": { + "source": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "target": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d" + } + }, + "description": "", + "id": "094b888c-450c-5c4f-971a-7d75db059574", + "name": "Exchange allocation: Request field repair" + }, + "0c4c9e58-ff32-5c00-b82a-d408a4be0183": { + "alias": "oa_mission_participant", + "body": { + "MissionInvolvement": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90" + } + }, + "description": "", + "id": "0c4c9e58-ff32-5c00-b82a-d408a4be0183", + "name": "Mission involvement: Maintain safe water supply" + }, + "109e2d6b-7e86-5934-93a5-fab56eced84e": { + "alias": "oa_assess_then_decide", + "body": { + "SequenceLink": { + "guard": "", + "source": "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "target": "b280c6ca-e5f9-5424-a5c1-5676e01c2371" + } + }, + "description": "", + "id": "109e2d6b-7e86-5934-93a5-fab56eced84e", + "name": "Continue" + }, + "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a": { + "alias": "oa_reset_allocation", + "body": { + "Allocation": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "d0c8be87-d238-503a-a549-7558a210507f" + } + }, + "description": "", + "id": "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a", + "name": "Allocation: Reset valve remotely" + }, + "1f8b1898-65f6-5360-99cf-b78a71bd50ec": { + "alias": "oa_mission_delivery", + "body": { + "MissionCapability": { + "source": "c9c61f50-06e0-5d17-973d-dcc57cd5cc90", + "target": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb" + } + }, + "description": "", + "id": "1f8b1898-65f6-5360-99cf-b78a71bd50ec", + "name": "Mission capability: Restore a safe supply" + }, + "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97": { + "alias": "oa_normal_transition", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "repair confirmed and pressure is safe", + "source": "58c15833-6e75-523a-b57f-a88f19f6d672", + "target": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "transition_kind": "External", + "trigger": "", + "triggers": [] + } + }, + "description": "", + "id": "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97", + "name": "State transition: Monitoring" + }, + "229ee94f-d147-50f6-9769-b5bd0c03a084": { + "alias": "oa_confirm_path", + "body": { + "SequenceLink": { + "guard": "", + "source": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "target": "e00594a3-6122-5a6b-b3c2-ecb5506c741d" + } + }, + "description": "", + "id": "229ee94f-d147-50f6-9769-b5bd0c03a084", + "name": "Continue" + }, + "27b47887-baf0-518b-bf87-f5acbf7d0b93": { + "alias": "oa_contact", + "body": { + "CommunicationMean": { + "items": [ + "d736c7d6-d369-550a-8ba4-dae223e40bb3" + ], + "oriented": false, + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49" + } + }, + "description": "", + "id": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "name": "Technician service conversation" + }, + "2b00270c-79a5-52a5-af96-33346bea531b": { + "alias": "oa_start_monitoring", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "", + "source": "a7ffb869-9dde-5e76-b276-c719c5c143a9", + "target": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "transition_kind": "External", + "trigger": "", + "triggers": [] + } + }, + "description": "", + "id": "2b00270c-79a5-52a5-af96-33346bea531b", + "name": "State transition: Monitoring" + }, + "3f54d890-4ca5-5e36-873b-abb5c92d578c": { + "alias": "oa_request_then_repair", + "body": { + "SequenceLink": { + "guard": "", + "source": "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "target": "c59432fe-6092-5ee0-908d-9dbf39476471" + } + }, + "description": "", + "id": "3f54d890-4ca5-5e36-873b-abb5c92d578c", + "name": "Sequence link: Repair the valve" + }, + "4549d3fa-5ab9-52a9-b4f9-3a78fc9d625a": { + "alias": "oa_report_carriage", + "body": { + "ExchangeAllocation": { + "source": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "target": "b436d0b3-c824-5a96-af6b-2c269816d1cc" + } + }, + "description": "", + "id": "4549d3fa-5ab9-52a9-b4f9-3a78fc9d625a", + "name": "Exchange allocation: Report repair result" + }, + "594c61cd-94e0-5c07-890d-edfd520d7df9": { + "alias": "oa_assess_allocation", + "body": { + "Allocation": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "61f7bbfb-74a4-5540-9182-2583dfb27a49" + } + }, + "description": "", + "id": "594c61cd-94e0-5c07-890d-edfd520d7df9", + "name": "Allocation: Assess pressure alarm" + }, + "628cb4fc-fe63-5f6b-b364-6268f26b5740": { + "alias": "oa_assessment_delivery", + "body": { + "ChainInvolvement": { + "source": "022d3cfa-bbaf-5e13-924a-8e55099b8745", + "target": "6a30b763-cc51-546d-907f-5c63657cfd03" + } + }, + "description": "", + "id": "628cb4fc-fe63-5f6b-b364-6268f26b5740", + "name": "Chain involvement: Assess an alarm" + }, + "76218d89-68fd-531d-b35b-90c5b769a5ef": { + "alias": "oa_remote_path", + "body": { + "SequenceLink": { + "guard": "remote reset is authorized", + "source": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "target": "d2026fc2-74a8-5e22-9942-ff3eb68a3677" + } + }, + "description": "", + "id": "76218d89-68fd-531d-b35b-90c5b769a5ef", + "name": "remote reset is authorized" + }, + "7fa37e63-db65-5aa7-88f2-31ff4347a787": { + "alias": "oa_assess_on_entry", + "body": { + "StateAction": { + "phase": "Entry", + "source": "58c15833-6e75-523a-b57f-a88f19f6d672", + "target": "61f7bbfb-74a4-5540-9182-2583dfb27a49" + } + }, + "description": "", + "id": "7fa37e63-db65-5aa7-88f2-31ff4347a787", + "name": "Assess when repair is needed" + }, + "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2": { + "alias": "oa_repair_allocation", + "body": { + "Allocation": { + "source": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "target": "a21465af-05f3-5fb5-8983-de26932422fb" + } + }, + "description": "", + "id": "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2", + "name": "Allocation: Repair valve in the field" + }, + "b436d0b3-c824-5a96-af6b-2c269816d1cc": { + "alias": "oa_repair_report", + "body": { + "Interaction": { + "items": [ + "d736c7d6-d369-550a-8ba4-dae223e40bb3" + ], + "source": "a21465af-05f3-5fb5-8983-de26932422fb", + "target": "727c0b5c-8db6-5eb7-83d6-735519c5949f" + } + }, + "description": "", + "id": "b436d0b3-c824-5a96-af6b-2c269816d1cc", + "name": "Report repair result" + }, + "bac43d3b-910b-5bdc-be9b-f78cf973e1ed": { + "alias": "oa_remote_merge", + "body": { + "SequenceLink": { + "guard": "", + "source": "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "target": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d" + } + }, + "description": "", + "id": "bac43d3b-910b-5bdc-be9b-f78cf973e1ed", + "name": "Continue" + }, + "bfd4326e-ea65-5269-b5e6-e063a355d1e9": { + "alias": "oa_confirm_allocation", + "body": { + "Allocation": { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "727c0b5c-8db6-5eb7-83d6-735519c5949f" + } + }, + "description": "", + "id": "bfd4326e-ea65-5269-b5e6-e063a355d1e9", + "name": "Allocation: Confirm safe pressure" + }, + "c1adbbd0-a762-5ee3-8b9a-5c18af377771": { + "alias": "oa_field_path", + "body": { + "SequenceLink": { + "guard": "else", + "source": "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "target": "fc6763bf-a534-54bf-afcd-57ab285ff07d" + } + }, + "description": "", + "id": "c1adbbd0-a762-5ee3-8b9a-5c18af377771", + "name": "else" + }, + "c7c248f8-28b8-55be-9023-0991d44021e3": { + "alias": "oa_response_delivery", + "body": { + "ChainInvolvement": { + "source": "58bae2b0-f560-50f2-818d-19091d22d18a", + "target": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb" + } + }, + "description": "", + "id": "c7c248f8-28b8-55be-9023-0991d44021e3", + "name": "Chain involvement: Restore a safe supply" + }, + "c8196d35-2168-5fef-8b03-ba0d139bc00f": { + "alias": "oa_repair_then_report", + "body": { + "SequenceLink": { + "guard": "", + "source": "c59432fe-6092-5ee0-908d-9dbf39476471", + "target": "df2fd3f8-6bc2-5380-a13f-36643ed35ac7" + } + }, + "description": "", + "id": "c8196d35-2168-5fef-8b03-ba0d139bc00f", + "name": "Sequence link: Report the measured result" + }, + "c8593670-8906-530f-b6a2-3ada77a9c219": { + "alias": "oa_field_merge", + "body": { + "SequenceLink": { + "guard": "", + "source": "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "target": "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d" + } + }, + "description": "", + "id": "c8593670-8906-530f-b6a2-3ada77a9c219", + "name": "Continue" + }, + "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d": { + "alias": "oa_repair_request", + "body": { + "Interaction": { + "items": [ + "d736c7d6-d369-550a-8ba4-dae223e40bb3" + ], + "source": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "target": "a21465af-05f3-5fb5-8983-de26932422fb" + } + }, + "description": "", + "id": "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "name": "Request field repair" + }, + "e21d9a54-ae8e-5618-a4f8-67ee22b92638": { + "alias": "oa_include_triage", + "body": { + "CapabilityInclude": { + "source": "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "target": "6a30b763-cc51-546d-907f-5c63657cfd03" + } + }, + "description": "", + "id": "e21d9a54-ae8e-5618-a4f8-67ee22b92638", + "name": "Capability include: Assess an alarm" + }, + "fbde9d5e-c7e9-55db-a2be-497345c491b3": { + "alias": "oa_alarm_transition", + "body": { + "StateTransition": { + "effect": "", + "effects": [], + "guard": "", + "source": "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "target": "58c15833-6e75-523a-b57f-a88f19f6d672", + "transition_kind": "External", + "trigger": "", + "triggers": [ + "65d6b8d6-271a-5c84-8b42-abbe310c955e" + ] + } + }, + "description": "", + "id": "fbde9d5e-c7e9-55db-a2be-497345c491b3", + "name": "State transition: Awaiting repair" + }, + "ffdbfcd0-d423-5707-a880-0dfbe1400f72": { + "alias": "oa_assessment_support", + "body": { + "CapabilityInvolvement": { + "source": "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "target": "6a30b763-cc51-546d-907f-5c63657cfd03" + } + }, + "description": "", + "id": "ffdbfcd0-d423-5707-a880-0dfbe1400f72", + "name": "Capability involvement: Assess an alarm" + } + }, + "schema_version": 6, + "traces": {} +} diff --git a/examples/release1/transition-request.json b/examples/release1/transition-request.json new file mode 100644 index 00000000..b2cc442b --- /dev/null +++ b/examples/release1/transition-request.json @@ -0,0 +1,114 @@ +{ + "communication_endpoints": [ + { + "source": "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "source_port": { + "Create": { + "alias": "sa_control_port", + "description": "Explicitly chosen Standard component port for the repair service.", + "id": "98d411e7-7c65-5c3f-b543-8bf0f2522ea2", + "kind": "Standard", + "name": "Repair service client", + "orientation": "Unset" + } + }, + "target_port": { + "Create": { + "alias": "sa_technician_port", + "description": "Explicitly chosen Standard component port for the repair service.", + "id": "e6d69cbd-99d7-596b-bbe3-dada6118a600", + "kind": "Standard", + "name": "Repair service provider", + "orientation": "Unset" + } + } + } + ], + "destination_diagram": null, + "direction": "OaToSa", + "fields": [], + "mappings": [ + { + "source": "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "target": "435bf87c-73fd-5e4d-978a-4c471bc9eaf0", + "target_kind": null + }, + { + "source": "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "target": "c05b9422-b9da-5de5-b21d-07ad23bd2b8a", + "target_kind": null + } + ], + "resolutions": [], + "scope": "SelectedOnly", + "sources": [ + "022d3cfa-bbaf-5e13-924a-8e55099b8745", + "0b07b9ed-492d-57f6-b0b1-ce8a709238a7", + "1970608f-85d1-5234-ac86-5e2b0df27dc3", + "19bd3a3a-4d0c-56d5-bf50-a6b43fd81771", + "1b91f8a7-0bdb-514e-b1cd-955658e09927", + "2011b3ac-614b-5a47-8891-7ff81965f425", + "20472233-5ce6-5ffe-b4ad-0bdbccf3e423", + "32aa9940-df6e-5a12-b9bd-6337e2d2ed22", + "3dfee23a-148b-5cec-ad2d-b774e84ea5bb", + "41859a65-ded2-547b-ab03-14e2fa740cf9", + "58bae2b0-f560-50f2-818d-19091d22d18a", + "58c15833-6e75-523a-b57f-a88f19f6d672", + "5ba8079c-2dbe-5e98-ae82-2b60c6020a02", + "5d3af417-244c-54bb-9588-16daedda4e1f", + "61f7bbfb-74a4-5540-9182-2583dfb27a49", + "65d6b8d6-271a-5c84-8b42-abbe310c955e", + "6a30b763-cc51-546d-907f-5c63657cfd03", + "6b945a6d-6aa0-5dd3-96e6-2e8d757d167d", + "6cbde0d8-9f5f-510a-a8c1-e29cb99ad4ae", + "727c0b5c-8db6-5eb7-83d6-735519c5949f", + "8412e9c6-96fe-5ed6-a903-1d37ec3c3bf7", + "8bc6d0a4-3e87-559b-8bd7-1b679a2db5b7", + "9349b353-53f2-56f8-b571-a05b09335cdf", + "942975b8-0bef-5ae8-9fcb-c37497833fb0", + "9ff94014-afa1-50ab-a4d3-8e9dd0565cc5", + "a21465af-05f3-5fb5-8983-de26932422fb", + "a7ffb869-9dde-5e76-b276-c719c5c143a9", + "b280c6ca-e5f9-5424-a5c1-5676e01c2371", + "b97437c6-cd7a-5a05-b537-b30a17dd936a", + "c59432fe-6092-5ee0-908d-9dbf39476471", + "c9c61f50-06e0-5d17-973d-dcc57cd5cc90", + "cc2d497a-fd15-5455-9ee5-e70eb95811d8", + "d0c8be87-d238-503a-a549-7558a210507f", + "d2026fc2-74a8-5e22-9942-ff3eb68a3677", + "d6f881a4-95da-54e9-ad97-1b8965532629", + "d736c7d6-d369-550a-8ba4-dae223e40bb3", + "df2fd3f8-6bc2-5380-a13f-36643ed35ac7", + "e00594a3-6122-5a6b-b3c2-ecb5506c741d", + "e07c3952-4a1a-5cc7-b7d5-03a01101ce49", + "e7824764-5db9-5384-8198-2fbaea2b10b2", + "fc6763bf-a534-54bf-afcd-57ab285ff07d", + "fc81c5f0-0442-5b88-992b-e5b10f351c59", + "fffff882-78fd-5415-928d-c1ef7fac6576", + "0c4c9e58-ff32-5c00-b82a-d408a4be0183", + "109e2d6b-7e86-5934-93a5-fab56eced84e", + "1853df1f-30f4-5b1a-baac-2dcaf73c7b3a", + "1f8b1898-65f6-5360-99cf-b78a71bd50ec", + "21fe5e8e-cbc2-5dad-b3e8-1df46e167d97", + "229ee94f-d147-50f6-9769-b5bd0c03a084", + "27b47887-baf0-518b-bf87-f5acbf7d0b93", + "2b00270c-79a5-52a5-af96-33346bea531b", + "3f54d890-4ca5-5e36-873b-abb5c92d578c", + "594c61cd-94e0-5c07-890d-edfd520d7df9", + "628cb4fc-fe63-5f6b-b364-6268f26b5740", + "76218d89-68fd-531d-b35b-90c5b769a5ef", + "7fa37e63-db65-5aa7-88f2-31ff4347a787", + "ae53f363-0dec-5a7c-9afd-8fcd1b778ab2", + "b436d0b3-c824-5a96-af6b-2c269816d1cc", + "bac43d3b-910b-5bdc-be9b-f78cf973e1ed", + "bfd4326e-ea65-5269-b5e6-e063a355d1e9", + "c1adbbd0-a762-5ee3-8b9a-5c18af377771", + "c7c248f8-28b8-55be-9023-0991d44021e3", + "c8196d35-2168-5fef-8b03-ba0d139bc00f", + "c8593670-8906-530f-b6a2-3ada77a9c219", + "cf423ea2-8b2e-5a27-93d4-5e8542d9db8d", + "e21d9a54-ae8e-5618-a4f8-67ee22b92638", + "fbde9d5e-c7e9-55db-a2be-497345c491b3", + "ffdbfcd0-d423-5707-a880-0dfbe1400f72" + ] +} diff --git a/examples/release1/views/diagram_oa_allocation_view.arcadia b/examples/release1/views/diagram_oa_allocation_view.arcadia new file mode 100644 index 00000000..0c34ca06 --- /dev/null +++ b/examples/release1/views/diagram_oa_allocation_view.arcadia @@ -0,0 +1,100 @@ +diagram oa_allocation_view "1 · OA responsibility and repair traffic" id "103ea62c-ac56-5827-94e7-cf09bca96ccc" { + description "Teaching view: positions and filters do not change ownership or exchange identities."; + layer Oa; + projection Allocation; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; +} + +node oa_allocation_view_oa_assess id "5ae1c46d-59a3-50fd-8cc3-b292df21f951" { + collapsed false; + diagram @oa_allocation_view; + element @oa_assess; + position { + x 100.0; + y 170.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node oa_allocation_view_oa_confirm id "edd74060-ac61-5450-a57c-a6577daa5d50" { + collapsed false; + diagram @oa_allocation_view; + element @oa_confirm; + position { + x 100.0; + y 490.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node oa_allocation_view_oa_control id "3191e05c-1c7f-5f0e-beb0-5f3bded54c48" { + collapsed false; + diagram @oa_allocation_view; + element @oa_control; + position { + x 60.0; + y 80.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node oa_allocation_view_oa_repair id "202ebe37-ddb4-5099-8ce9-ec3e895ab9e9" { + collapsed false; + diagram @oa_allocation_view; + element @oa_repair; + position { + x 890.0; + y 170.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node oa_allocation_view_oa_reset id "7066f78c-9736-5349-b94a-0587331b980e" { + collapsed false; + diagram @oa_allocation_view; + element @oa_reset; + position { + x 100.0; + y 330.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node oa_allocation_view_oa_technician id "d27f28ef-f10a-5355-9a05-6f2fd39b7de6" { + collapsed false; + diagram @oa_allocation_view; + element @oa_technician; + position { + x 850.0; + y 80.0; + }; + size { + height 110.0; + width 245.0; + }; +} + diff --git a/examples/release1/views/diagram_sa_allocation_view.arcadia b/examples/release1/views/diagram_sa_allocation_view.arcadia new file mode 100644 index 00000000..b3f72d22 --- /dev/null +++ b/examples/release1/views/diagram_sa_allocation_view.arcadia @@ -0,0 +1,100 @@ +diagram sa_allocation_view "2 · SA service boundary and functions" id "35d3c24b-289e-59e5-a610-8333e2c6a4bb" { + description "Teaching view: positions and filters do not change ownership or exchange identities."; + layer Sa; + projection Allocation; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; +} + +node sa_allocation_view_sa_assess id "60f6ad74-b69e-5aef-b832-f1bd60b903ae" { + collapsed false; + diagram @sa_allocation_view; + element @sa_assess; + position { + x 100.0; + y 170.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_allocation_view_sa_confirm id "7e3ba36f-bc68-5520-bdb9-77083bb7bba2" { + collapsed false; + diagram @sa_allocation_view; + element @sa_confirm; + position { + x 100.0; + y 490.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_allocation_view_sa_control id "8345c76a-8d85-5c31-902e-ec1cd33ed4a3" { + collapsed false; + diagram @sa_allocation_view; + element @sa_control; + position { + x 60.0; + y 80.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_allocation_view_sa_repair id "34630a18-41b0-5fdd-bff4-ee86e93e8651" { + collapsed false; + diagram @sa_allocation_view; + element @sa_repair; + position { + x 890.0; + y 170.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_allocation_view_sa_reset id "73406244-ebd7-5995-86c7-e17c73c50b32" { + collapsed false; + diagram @sa_allocation_view; + element @sa_reset; + position { + x 100.0; + y 330.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_allocation_view_sa_technician id "f7225d1c-e506-505d-bb20-916950269d4b" { + collapsed false; + diagram @sa_allocation_view; + element @sa_technician; + position { + x 850.0; + y 80.0; + }; + size { + height 110.0; + width 245.0; + }; +} + diff --git a/examples/release1/views/diagram_sa_capability_view.arcadia b/examples/release1/views/diagram_sa_capability_view.arcadia new file mode 100644 index 00000000..0b6b78ed --- /dev/null +++ b/examples/release1/views/diagram_sa_capability_view.arcadia @@ -0,0 +1,72 @@ +diagram sa_capability_view "4 · SA mission and included capability" id "3e6ad194-61b5-584c-b0f0-a78a20af5781" { + description "Teaching view: positions and filters do not change ownership or exchange identities."; + layer Sa; + projection Capabilities; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; +} + +node sa_capability_view_sa_control id "41ddb68b-bd2b-5f91-82eb-efde01157828" { + collapsed false; + diagram @sa_capability_view; + element @sa_control; + position { + x 60.0; + y 60.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_capability_view_sa_mission id "4b814335-bf98-5b68-85d5-ea549766517a" { + collapsed false; + diagram @sa_capability_view; + element @sa_mission; + position { + x 420.0; + y 60.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_capability_view_sa_restore id "d3bc13f9-7726-5280-83fe-63f391c45afb" { + collapsed false; + diagram @sa_capability_view; + element @sa_restore; + position { + x 420.0; + y 260.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_capability_view_sa_triage id "069a3fb3-077c-59d8-91da-27b0665143c7" { + collapsed false; + diagram @sa_capability_view; + element @sa_triage; + position { + x 800.0; + y 260.0; + }; + size { + height 110.0; + width 245.0; + }; +} + diff --git a/examples/release1/views/diagram_sa_service_view.arcadia b/examples/release1/views/diagram_sa_service_view.arcadia new file mode 100644 index 00000000..48576c7b --- /dev/null +++ b/examples/release1/views/diagram_sa_service_view.arcadia @@ -0,0 +1,104 @@ +diagram sa_service_view "3 · SA service conversation only" id "50af4c9b-93ce-5ff5-a1f8-7a81eb97e779" { + description "Teaching view: positions and filters do not change ownership or exchange identities."; + layer Sa; + projection Allocation; + routes []; + view_options { + coverage "All"; + exchange_labels "NameAndItems"; + hidden_edges []; + participant_categories { + Any [@sa_contact_category]; + }; + relation_kinds "All"; + work_categories { + Any []; + }; + work_contexts "All"; + }; +} + +node sa_service_view_sa_assess id "439f1e45-e5ed-567c-978b-4c93ef267db1" { + collapsed false; + diagram @sa_service_view; + element @sa_assess; + position { + x 100.0; + y 170.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_service_view_sa_confirm id "6f138672-149b-5f88-ab07-7fc413a42262" { + collapsed false; + diagram @sa_service_view; + element @sa_confirm; + position { + x 100.0; + y 490.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_service_view_sa_control id "f53d4352-b042-56a6-8b3e-c51cd27254f9" { + collapsed false; + diagram @sa_service_view; + element @sa_control; + position { + x 60.0; + y 80.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_service_view_sa_repair id "ae36489d-6ec8-5f1c-b603-8b497227efba" { + collapsed false; + diagram @sa_service_view; + element @sa_repair; + position { + x 890.0; + y 170.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_service_view_sa_reset id "d52da4e6-40b7-5dbc-84fb-80c443711c69" { + collapsed false; + diagram @sa_service_view; + element @sa_reset; + position { + x 100.0; + y 330.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_service_view_sa_technician id "40210589-69f7-5209-b329-10bb83cf615c" { + collapsed false; + diagram @sa_service_view; + element @sa_technician; + position { + x 850.0; + y 80.0; + }; + size { + height 110.0; + width 245.0; + }; +} + diff --git a/examples/release1/views/diagram_sa_state_view.arcadia b/examples/release1/views/diagram_sa_state_view.arcadia new file mode 100644 index 00000000..6bbb6df1 --- /dev/null +++ b/examples/release1/views/diagram_sa_state_view.arcadia @@ -0,0 +1,90 @@ +diagram sa_state_view "5 · SA pressure supervision" id "30d5baf9-6581-5cb6-846a-bcaa1e6cce63" { + description "Teaching view: positions and filters do not change ownership or exchange identities."; + layer Sa; + projection { + StateMachine { + machine @sa_monitor; + }; + }; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; +} + +node sa_state_view_sa_monitor id "a676fbb9-0833-59a8-98cf-a10d8f4b7434" { + collapsed false; + diagram @sa_state_view; + element @sa_monitor; + position { + x 30.0; + y 30.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_state_view_sa_monitor_initial id "5e2326a0-0f6c-5f8d-8a21-fdded18a9281" { + collapsed false; + diagram @sa_state_view; + element @sa_monitor_initial; + position { + x 100.0; + y 200.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_state_view_sa_monitor_region id "55a74f91-2025-5001-8db8-9566b1dbe783" { + collapsed false; + diagram @sa_state_view; + element @sa_monitor_region; + position { + x 70.0; + y 95.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_state_view_sa_monitoring id "b3af7b04-5a45-5b82-ab69-7eada4b876f0" { + collapsed false; + diagram @sa_state_view; + element @sa_monitoring; + position { + x 220.0; + y 200.0; + }; + size { + height 110.0; + width 245.0; + }; +} + +node sa_state_view_sa_waiting id "57acbb71-ca86-57fc-ae90-42dad195f0e0" { + collapsed false; + diagram @sa_state_view; + element @sa_waiting; + position { + x 650.0; + y 200.0; + }; + size { + height 110.0; + width 245.0; + }; +} + diff --git a/harness/DECISIONS.md b/harness/DECISIONS.md index 3186a900..79119a74 100644 --- a/harness/DECISIONS.md +++ b/harness/DECISIONS.md @@ -202,3 +202,17 @@ alternatives and falsification tests. This is a representation decision, not an implemented or accepted feature; versioning and view contracts remain to be integrated with it. Revisit if realistic OA workflows demonstrate separately identified operational endpoint contracts that a participant relation cannot carry. + +## Source6 saved views and cross-layer Traceability — 2026-09-06 + +Problem: saved selective views need explicit inclusion intent without changing semantic objects, and a Traceability view already displays both OA and SA. Applying ordinary layer filtering to it would silently remove existing counterpart edges. + +Decision: retain same-layer admission for ordinary diagrams, but allow both-layer edge, category and work-context references in Traceability. Shared projection and validation choose the cross-layer mode together; selectors expose both layers. Default options preserve prior visibility. Categories are independent Work/Participant families; explicit empty selectors mean none, never a silent reset to All. A document-revision semantic index is reused by canvas projections and invalidated on accepted changes/Undo/Redo. New source6 fields and kinds cannot enter frozen1–5 input contracts. + +Alternatives rejected: filtering every view by its labelled layer loses existing Traceability content; ignoring layer validation everywhere hides mistaken references; serializing temporary GUI filters breaks source/API parity. Revisit if native workflows or cross-layer regression evidence exposes ambiguity. Current implementation is pending integrated tests and native acceptance, not an established release result. + +## Behavior transition destination context — 2026-09-06 + +Native reverse scenario Apply selected a dependency and inserted five overlapping behavior placements into an unrelated architecture view. Core transition correctness did not make that presentation useful. Select the counterpart of the explicit request root and resolve its nearest scenario/process/chain/state owner. Without an explicit destination diagram, keep the exact core candidate and open Behavior; preserve unrelated architecture views. Retain ordinary architecture transition placement and honor explicit diagram choices. + +Automatically creating another architecture view would retain the wrong editing workspace and introduce unrequested model state. Merely navigating to Behavior after adding placements would conceal the presentation damage. Both alternatives are rejected. The selected correction retains one source transaction and Undo operation and guards stale canvas events. Revisit if mixed-root requests or realistic explicit destination workflows require a separate user choice; do not infer such a choice from dependency ordering. Test/build/native acceptance is recorded separately in release-behavior-transition-context-20260906.md. diff --git a/harness/PLAN.md b/harness/PLAN.md index 97837a6e..9964dfba 100644 --- a/harness/PLAN.md +++ b/harness/PLAN.md @@ -80,3 +80,11 @@ Destination and catalog defects found afterward are fixed/reviewed/integrated; recheck those and Linux native next. Constructor groups and reset/unordered keyed GUI are integrated/reviewed. Annotation/Binding and Home/recents are active; order, attachments, artifacts, multi-source GUI and checkpoint recovery remain required. + +## Current execution refinement — 2026-09-06 + +First-release scope above is unchanged. Current work integrates source6 participant communication, typed saved views and batch semantic review. Run the integrated desktop test build and decisive compatibility/recovery/convergence/projection tests first. Then evaluate actual new native workflows and exact source/REST oracles. Add presentation duplication through existing CreateDiagram; test the deterministic forty-same-pair exchange fixture and implement explicit presentation grouping only if its readability/identity-access gate fails. Remaining child/incident/artifact review controls and full platform/package/end-state gates follow. Current evidence and process ownership are in the top section of STATE.md; historical statuses below do not describe current running binaries. + +The later source6 checkpoint has native artifact/batch/attachment/communication/view/history evidence and corrected behavior-transition navigation. Teaching onboarding and reviewed-name source/history/reopen pass; a separate semantic-review view mismatch is corrected and under rebuilt native verification. Linux server build and actual HTTP/restart pass on an immutable older lesson snapshot; full desktop/package acceptance remains required. + +Scope reconciliation established two remaining semantic gaps through executable counterexamples: S1 contextual links into repeated/nested reusable processes and S2 explicit interface-operation selection in parameterless scenarios. Complete their jointly reviewed source7 contract before implementation: one core owner coordinates DTO/resolver/validation/reference lifecycle/source/version/transition/facets; separate desktop and recovery owners consume those shared semantics after the core checkpoint. Preserve old5/6 byte and checkpoint contracts, typed graphical/source/REST parity, caller-specific diagnostics and real instance-isolation/lifecycle oracles. Improve the cramped process working space and explicit reference navigation alongside contextual rendering. These are release work, not optional later-layer features. Independent critiques and proposed analysis rules are linked from STATE.md. diff --git a/harness/STATE.md b/harness/STATE.md index e356521b..74a2ef50 100644 --- a/harness/STATE.md +++ b/harness/STATE.md @@ -1,130 +1,52 @@ -# Current continuation — 2026-09-06 +# Progress Ledger — first release -The full first-release goal remains active. User requested that completed Git branches be accounted for: current branch codex/release1-continuation starts from integrated c091f80; the branch audit proves meaningful side-branch work is included or patch-equivalent. No stale stash is restored. +Current checkpoint: 2026-09-06 evening, after corrected reverse-transition native acceptance, teaching source/history/reopen checks, and actual Linux server build/HTTP restart acceptance. **Goal active; release incomplete.** The authoritative objective is `C:\Users\User\.codex\attachments\10b0f701-1743-4de7-9ee1-225920e19649\goal-objective.md`: a usable, feature-complete OA/SA engineering tool for Windows and Linux with graphical/source/REST parity, artifact traceability, offline guidance and independent end-state acceptance. -Current Windows evidence: 869 workspace tests pass with one existing ignored benchmark (.local/current-integrated-tests-corrected.log). Strict all-target Clippy and clean GUI/server builds pass (.local/current-integrated-clippy.log, .local/current-integrated-build-clean.log). Two equivalent guard rewrites and a test-only helper annotation closed Clippy findings after the passing test run. Shared Windows Cargo target is idle. Portable Rust1.90 GNU host plus gnullvm target is prepared; MSRV compilation is still pending. +## Workspace and ownership -Native process10896 is running from .local/native-qa/bin-20260906-corrected with isolated appdata and system-only PATH. Binary660BECC8DB96B218D12805B39661B46C9F48DF4F8F07CB3B7D42C1F803BE089F includes all behavior/Home/save/layout fixes; the later clean build differs only in the stated lint/test-helper changes. Computer Use was stopped with Escape, then explicitly reauthorized by the user's @Computer message. The unfinished timed-transition checkpoint recovered exactly, including invalid empty expression. Repair30s created one Event and changed its Internal transition atomically. Save-as to state-recovered-renamed.arcadia, one Undo, Save, Redo, Save passed: Undo entry exactly pristine; Redo entry exactly accepted; checkpoints retired. Home Continue editing returns to the same Behavior state view. Minimum1100x700 navigation shows all9pages and guide Clear is no longer clipped. Root is currently on the guide; ordered and reference-pair layout native checks remain. +- Current branch `codex/release1-continuation`, HEAD `2b2d2bc18bc11ca71ef8c4c5d26610939c1b1f42`; source-6 continuation contains substantial uncommitted work. Integrated ancestry starts at `c091f80`; [branch audit](release-branch-integration-20260906.md) prevents reimplementing already integrated data, state/scenario/process, hosted and semantic work. +- Root is sole native operator. Latest process **9672** ran `.local/native-qa/bin-source6-behavior-context`, GUI SHA-256 `F5ECAEC02BBAFBC6F3BDC6ADEB746AFE91F3A495C6D9564A68DEA41E61AC9246`, and closed cleanly for the next rebuild. Corrected reverse transition and teaching Home/guide/raw guard/review/source/history/reopen passed on this binary; the newly found semantic-review canvas navigation fix is building separately. +- QEMU second gate is active, PID10880, one vCPU/2GiB/BelowNormal, launched20:33PDT with90-minute bounded budget and clean-poweroff reserve. State-contract owns guest Cargo/server runtime validation. Snapshot281files SHA256394197749a01afe5a6e911f0ad4594bfef034c0da4cdc7ffd96454354a51c5c6 includes embedded docs; predates the latest teaching/navigation corrections. WSL remains unavailable pending restart; no host restart is authorized. +- Behavior-review owns semantic-review navigation and S2 critique. State-representation completed S1 critique; state-contract owns guest Cargo and its Linux report. Guest compiler briefly paused21:31PDT for the bounded Windows navigation tests/build, with unchanged35-minute test/90-minute guest deadlines. No concurrent Cargo writers; user-local Windows environment: `. .local/toolchain/env.ps1`. -REST: the staged server passed25 captured requests, twice from isolated stores, with exact five coverage warnings across source/JSON/current validation, atomic422 rejection, repaired persistence after forced restart and warning restoration by explicit replacement. Reproducible probe/fixture is tracked in harness/fixtures/rest-coverage. This is Windows service evidence, not Linux acceptance. +## Completed and verified at this checkpoint -Packaging: scripts stage selected dependency/runtime notices and verify source copies and every ZIP entry. QA b has528files including485licensefiles; ZIP SHA256237b30ee0c704efad89ed5c371d2c965f9e7d11ea318c97ba16ee34282456f73. Iced Icons uses its declared distributed MIT plus preserved font copyright metadata and candid provenance limits; no contrary license was established. See release-windows-package-review.md for exact clean-build hashes. Earlier QA package native clean-PATH Home/example/help/close passed; final QA b native acceptance remains. +| Scope | Evidence and limit | +|---|---| +| Latest Windows diagnostics and guide | Semantic filter **108 desktop + 2 core** passed; full desktop **527/527** passed; strict all-target Clippy and GUI/server build passed (`.local/semantic-diagnostics-final-tests.log`, `desktop-diagnostics-final-tests.log`, `diagnostics-final-clippy.log`, `diagnostics-final-build.log`). Subsequent guide **3/3** focused tests, strict Clippy and build passed (`.local/guide-navigation-{tests,clippy,build}.log`). The guide test increases compiled desktop inventory to528; no full528 run or final full-workspace total is claimed. | +| Current artifact implementation checkpoint | Full desktop **518/518**, corrected fresh artifact picker **6/6**, strict all-target Clippy passed. Logs `.local/source6-artifact-inspector-desktop-tests2.log`, `.local/source6-artifacts-final-tests2.log`, `.local/source6-artifacts-final-clippy3.log`. Four subsequent artifact recovery falsifiers and two core absence oracles passed separately. This predates latest display corrections. | +| Native artifact MOVE / CREATE / DELETE | All three complete source Apply/Save, one Undo/Save and one Redo/Save oracles passed; **nine saves succeeded first attempt**. MOVE invalid Owner and disabled optional text, and CREATE excluded row/all eight decisions/reserved UUID survived controlled-stop recovery exactly. DELETE removed destination from identified moved-source absence while retaining the source link. [Exact native report](release-artifacts-native-20260906.md), [fixture/generator instructions](fixtures/release1-native/artifacts/README.md). | +| Source-6 atomic batch review | Included valid and excluded invalid raw rows survived forced-stop recovery; repaired two-source Apply and complete Save/Undo/Redo source comparisons passed. [Native report](release-current-native-verification.md). | +| Scenario ordered-member variant | At1100×700, Take rejected ambiguous insertion; explicit final order retained the local self-test. Complete Apply/Save, Undo/Save and Redo/Save matched the independent source oracles with only the schema5→6 header token upgrade. All three saves succeeded first attempt. Branch steps/fragment branches and this variant's crash recovery remain separate cases. [Exact native report](release-current-native-verification.md). | +| Native guide navigation | On current guide binary at1100×700: related Scenarios-bottom→Exchanges opens the new heading; filtering `exchange` retains the current topic's bottom position; `Git` auto-selects Build first and opens its heading. No dirty model. Root's actual native observations close these bounded scroll cases; [implementation/tests](release-semantic-conflict-diagnostics-20260906.md) remain separately scoped. | +| Communication means | Drawing/create/reuse and legacy reverse full-model/source/history passes are retained. Corrected reverse Apply now opens the explicitly requested OA scenario in Behavior, preserving the original four-object Architecture. Distinct oracle predicts10UUIDs/zero placements; accepted model/all3sources, Undo baseline, Redo and reopen pass. Every Save succeeded first submission. Navigation5focused tests, strict Clippy and bins build pass; compiled desktop535 is not a full535 run. [Exact report](release-communication-native-20260906.md), [implementation](release-behavior-transition-context-20260906.md). | +| Teaching project and Home hook | Corrected94element/62relation/5view/68trace/30source example passes4expanded direct-rustc tests. At minimum size, Home/guide actions, whole-folder guidance, invalid raw source guard/Cancel/UndoText, located intended warning, reviewed NameTake and all37file exact Undo/Redo/reopen checks passed. New semantic navigation defect is under correction; complete process/state/data/view/evidence lesson remains open. [Native report](release-teaching-native-20260906.md). | +| Child/Incident and source-5 recovery | Two-row attachment recovery and complete Apply/Save/Undo/Redo passed, every save first attempt. Exact old source-5 raw/base/include/path retention, manual repair and two-file Save passed. [Attachments](release-attachments-native-20260906.md), [source-5 native](release-source5-native-recovery-20260906.md), [independent review](release-source5-checkpoint-independent-review-20260906.md). | +| Dense saved views | Forty-label baseline and selected real relation identities inspected; filter, duplicate/hide, original10/copy9 persistence across restart and exact model oracle passed. Additional coverage/context intersections, empty selections and Names-and-items preview/Apply/Undo observed. Minimum-size overview requires working zoom/pan. Limits remain in [native evidence](release-current-native-verification.md). | +| Core/source/REST integration | HTTP **48**, OpenAPI **39 fixtures / 197 schemas**, and source-5 focused **13** passed. Report scoped results until the final full-workspace run; do not total overlapping filters, platform reruns or new compiled-but-unrun tests. Historical aggregate accounting remains archived. | +| Actual Linux/MSRV core and server | Ubuntu24.04.4/x86_64/glibc2.39/ext4/Rust1.90: core38/38, patched journal4focused/42full and3explicit syscall fault cases pass on recorded snapshots. New actual server build passes34m42s including3m24s pause. Real HTTP modular import, invalid PUT unchanged source/ETag, clean SIGTERM and exact model/source/ETag restart pass on the older28-source lesson snapshot. Full server tests are running; current desktop/Linux package remain unverified. [Linux report](release-linux-validation-options.md), [journal contract](release-linux-journal-publication.md). | +| Packaging foundation | Portable Windows QA staging, dependency/runtime/font/Rust notices, archive readback and clean-PATH smoke passed at earlier binaries. [Package report](release-windows-package-review.md). QA archives are not current final release artifacts. | -Linux: the QEMU pilot achieved actual Ubuntu SSH/cloud-init/filesystem/DNS validation with no failed units. Guest-only startup timeouts were adapted for slow emulation; no application build ran. User then installed WSL and authorized creating a distro. Our QEMU processes are stopped. WSL2 Ubuntu24.04 import failed HCS_E_SERVICE_NOT_AVAILABLE; VMP is enabled but Windows reports a pending reboot, with vmcompute/hns absent. User was asked to restart when convenient. No existing distro was touched and no Windows features/settings were changed by the agent. Verified official image and exact isolated retry command are in release-linux-validation-options.md. Recheck after restart before concluding nested virtualization is unavailable. +Historical full platform/native milestones retain their original revision scope; [complete prior ledger](history/2026-09-06-ledgers-before-consolidation/STATE.md) preserves all evidence links and previous environments. A test or native pass never transfers automatically to a changed executable. -Required next work: ordered/keyed/native view checks and source-oracle capture; checkpoint verified work in Git; implement first-class OA CommunicationMean plus deliberate source6 admission, saved selective/category/context views, remaining attached-child/incident/artifact/multi-source review GUI, full engineering guide, MSRV/optimized packages and fresh Windows/Linux end-state acceptance. Selective-view candidate and independent critique now define family-specific category sets, global versus selected-context coverage, preserved port geometry and a dense40-edge readability gate. Nothing here narrows the original goal or marks the release complete. +## Active findings and checks -The material below records earlier checkpoints. This current section takes precedence for process state, tool ownership and current evidence. -Authoritative objective: attachment `10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md`, read in full. User requested accounting for other Git branches. Current working branch is `codex/release1-continuation` from integrated `c091f80`; all meaningful side-branch production work is present or patch-equivalent. See release-branch-integration-20260906.md. Original checkout `ea2e8e9` was stale; the duplicate investigation is stashed and superseded. +Native artifact review exposed same-row conflict wording and reserved identities shown as deleted. Corrections retain typed actual-slot equality, row-addressed reservations and accepted-identity precedence; independent re-review found no further material defect. The completed108-semantic/527-desktop checks supersede the earlier interrupted run and include reserved-label/partial-creation regressions. Revised artifact wording still needs its own native observation on the current binary. Guide-scroll implementation/tests/build and the bounded native cases above passed. [Diagnostic/guide report](release-semantic-conflict-diagnostics-20260906.md). -Historical paths/windows/builds below are evidence from the previous environment, not current active processes. This machine has no D:/CodexBuilds or installed WSL. The first release is incomplete. +**F1** teaching onboarding/review/history has current-binary native evidence; remaining tutorial work is scoped in its report. **F2**, faithful behavior-canvas SVG export, is deferred by root's scope decision: export remains architecture-only and must be described that way; no faithful scenario/process/state SVG acceptance is claimed. -Current implementation: Home/recents, semantic Annotation/Binding review, private accepted-source/raw-draft checkpoints including workflow recovery, and typed StateAction detail rows. Independent reviews found and closed checkpoint late-write/discard races, candidate-parent picker context and literal-UUID expression rendering defects. Native acceptance is still required. +Reverse scenario's first-dependency focus and five overlapping architecture placements are fixed and natively reverified. Teaching NameTake exposed a separate post-commit layer/view mismatch; a bounded shared navigation helper and4focused tests are implemented, current build pending. Minimum-size process working space is cramped; native lesson evidence records23% overview versus100% scrolling rather than claiming readability at Fit. -Current toolchain: user-local Rust 1.98.1 `stable-x86_64-pc-windows-gnullvm` plus official LLVM-MinGW 20260826 UCRT under ignored `.local/toolchain`; dot-source `.local/toolchain/env.ps1`. GNU test executables with installer-like filenames initially failed Windows error 740. Shared build scripts now embed an explicit `asInvoker` manifest in GNU/gnullvm binaries and tests, without changing system security or requiring elevation. +Independent scope reconciliation verified **S1** missing contextual assembly into repeated/nested process references and **S2** missing maintained interface-operation identity in parameterless messages. Isolated existing-core probes falsify the proposed workarounds. Proposed source7 contracts are not production features. Independent critique requires deterministic cache-independent admission, explicit whole-link deletion, purpose-specific analysis, and one frozen old5/6 message/checkpoint migration shared by both additions. [Scope reconciliation](release-scope-reconciliation-20260906.md), [S1 contract](release-chain-assembly-contract-probe-20260906.md), [S1 critique](release-chain-assembly-contract-independent-review-20260906.md), [S2 contract](release-interface-operation-contract-20260906.md). -Fresh workspace tests pass **840 tests** with one existing benchmark ignored; `.local/current-workspace-tests-final.log`. Strict all-target workspace Clippy passed in `.local/current-clippy.log`. Initial keyed fixture, core row-order and state test failures are fixed; a hosted timeout test's invalid admission-timing assumption is also corrected. Independent review fixed write/restore checkpoint workspace limits before a prior valid checkpoint can be replaced. +Linux ordinary rename's empty-target replacement was independently reproduced and corrected using `renameat2(RENAME_NOREPLACE)` without fallback. Exact errno/no-write assertions now pass under actual syscall injection. This establishes the ext4 publication invariant, not power-loss durability, hostile ancestor protection or model-file compare-and-swap. Windows retains its no-replace API and bounded scanner retries. -Current native Windows evidence is in `release-current-native-verification.md`: empty Home/default/minimum layout, example, rejected raw source crash/recover, exact21-file Save as, recovery retirement, Recents reopen, rejected raw UUID crash/recover and guarded Save & continue passed. All three typed StateAction rows select the correct relation/phase/target in Behavior. Native found a saved-state-view entry bug: Architecture uses a generic renderer and wrong tools. Routing fix passed435 desktop tests and GUI/server build. Updated native process796 is running; invalid Binding crash recovery, exact repaired Save and one-step Undo/Redo passed. Annotation workflow recovered and saved exactly against the oracle; native found Save-as basename rename adds an unwanted Undo step and can clear Redo. A bounded history-preserving correction is being implemented. +## Remaining gates and next actions -Root controls native input. Semantic worker currently owns shared Cargo for ordered-review focused tests; coverage diagnostics follow. Save-as history worker must coordinate build access. All native trial data lives under ignored `.local/native-qa`, with app metadata redirected away from the user's real recents. Current uncommitted continuation still needs native semantic/binding/history and behavior/workflow acceptance, fresh routing checks and a milestone commit. It is not a complete release. +1. Finish semantic-review navigation tests/rebuild/native replay; preserve completed reverse/teaching oracles. Synthesize independently critiqued S1/S2 contracts before coordinated core/source7 implementation, then provide complete graphical/source/REST/recovery parity. Native-check revised reserved labels and same-row conflicts; complete uncovered tutorial workflows. +2. Close uncovered selective-view lifecycle cases; advanced ordered/process/scenario/fragment variants beyond the accepted Scenario case; typed state actions and pending Linux data/hosted/compact workflows. Implemented semantics must not be called missing merely because acceptance is outstanding; [coverage audit](release-current-coverage-20260906.md) is the baseline map, newer scoped reports supersede its old gap claims. +3. Verify complete current Linux integration suites and desktop/server build/runtime, then supported-size native workflows. The successful core/MSRV pilot is a reusable starting point, not application acceptance. +4. Run optimized/MSRV/final platform gates, strict reST/docs/example/source/OpenAPI checks, actual hosted transport variants and final migration/package checks. Rebuild portable distributions from the accepted exact tree, validate notices/runtime/content/checksums and clean-environment launch before replacing old bundles. +5. Conduct fresh independent requirements/domain/parity/architecture/runtime/UX/end-state reviews; resolve material findings. Commit/integrate authorized source with reviewable scope; remote publication is not requested. Only complete the goal when its applicable criteria are met. -The current coverage audit is `release-current-coverage-20260906.md`. Independent OA design/critique agrees that ordinary participant communication needs a first-class CommunicationMean instead of broadening SA port-bound BoundaryExchange. Other remaining release work includes saved selective/coverage views, completeness diagnostics, ordered/attachment/artifact/multi-source review controls and final platform/package gates. These are required unfinished work, not exclusions. - ---- -# Current release state — 2026-09-06 - -The first-release goal remains active and incomplete. Authoritative objective: -attachment10134df9-bea2-4c02-a820-67b53d6b86f7/goal-objective.md. PLAN/TASK_LEDGER -retain complete OA/SA, graphical/source/includes/REST parity and release acceptance. - -## Ownership and integrated state - -- Main C:/Users/sorok/OneDrive/Documents/ChatGPT/Capella 2 is the continuation - pointer, codex/release1-engineering,4fa9591; production work is in DATA below. -- DATA D:/CodexBuilds/Capella2-data-integration, codex/data-contract-integration, - production14b8e18. Root owns index and Capella2-data-target (idle). -- Linux D:/CodexBuilds/Capella2-data-linux-review, codex/data-linux-review, - equivalent9c61d0d. Root owns index and data-linux-target (idle). -- SEM D:/CodexBuilds/Capella2-semantic-review, codex/semantic-review,6fb01ae. - semantic_gui_design owns slice2 Annotation/Binding GUI, index and - semantic-desktop-target. Slice1 exact reset/unordered Member/Literal is integrated - and independently reviewed; ordered/attachment/artifact/multi-source work remains. -- HOST D:/CodexBuilds/Capella2-hosted-desktop, codex/hosted-desktop,e807086. - semantic_integration owns Home/start/recents, index and scenario-target. Child - owns new Home tests; no overlap with root or SEM. Recovery is not implemented. -- Root is sole native operator. Agents may not issue native input. - -## Verified integration - -R6 data contracts, direct scenario/process gestures, source5 core/REST and scalar/ -constructor GUI are integrated. Full Windows767/Linux770 workspace tests passed -at DATA28ebbbb/Linux04210b3, one intentional benchmark ignore each; strict checks -and optimized desktop/server builds passed. Live REST184 schemas/19 typed values/ -9 malformed rejections passed. See scoped ledgers for limits. - -Newest DATA14b8e18/Linux9c61d0d:392 desktop tests each, strict all-target desktop -Clippy, workspace fmt and release builds pass. Sessions24877/10919 closed0. -Logs integrated-review-{test,clippy,build}.log in Windows data-target and Linux -checkout target. Initial Windows log redirect failed before Cargo (nonexistent -checkout target); rerun used the existing dedicated target and passed. - -Independent reviews closed constructorc149f2f (299 tests), reset/unordered6fb01ae -(311 tests), invalid-local-source destinationb04c4b3 and catalog12e3020/e807086. -Catalog review found and corrected false empty filtered results when Check observes -a changed head outside displayed rows; unchanged-head/cross-service controls pass. -Constructor and keyed core changes43370de/8a75294 have independent probes and359 -core tests passing. Full graphical parity is not yet established. - -## Native state and evidence - -Windows hosted6fcc238 trial71940 closed0/empty stderr. Default/minimum publish, -graph/text Save, invalid source retention, artifact API Check/review/Reload/Undo, -stale ETag conflict/local retention, offline/retry, exact three-file local Save As -and catalog Open passed. Isolated loopback serverPID20984 remains at127.0.0.1:8787, -resource d28b2fc9-fbbd-456b-8ba8-a1195d1c8f33 revision6. Evidence/store is -C:/Users/sorok/AppData/Local/Temp/capella2-hosted-native-20260906. - -Invalid local repair source used to retain hosted destination and overwrite that -resource after repair. Fix is integrated/reviewed; actual fixed native flow remains -pending. Catalog freshness is likewise integrated/reviewed, native check pending. - -Current Windows constructor trial84997/window4327540 uses frozen cf04ff1 at -ui-constructors-windows-cf04ff1, SHA86C86A0A4E9E7BDEB5BBB64F6787AA5D3A8715D109979A57D056AEB686538E38. -At1100x700, fixture01 Data reviewed Take, exact source/history, Undo/Redo and Save -passed; parsedProject matched independently core-generated expected. Candidate -Real retained raw '-' across Integer/Real changes, blocked incompatible Boolean -default, accepted repaired -1.5..100/default3.5, applied as one Undo/Redo and saved. -Current accepted file is fixtures/01-data-switch.arcadia with Real candidate; -native property values/history and disk comments/unitcm confirmed. Independent -candidate exactProject comparison is pending. No modal currently open. - -Remaining five pristine constructor fixtures and expected outputs are in -C:/Users/sorok/AppData/Local/Temp/capella2-constructor-native-20260906. Do not rerun -the UUID-generating fixture generator. Copy originals before editing. - -New frozen accepted builds (native pending) under Capella2-release1-01a06e2e: -ui-integrated-review-windows-14b8e18 and ui-integrated-review-linux-9c61d0d. -Manifests contain hashes. Earlier scalar native accepted; process camera/owner -accepted, compact pending form fixf08507a needs native recheck. See native ledgers. - -Linux uses Ubuntu-24.04, Rust1.98.1 PATH/home/sorokinoleg/.cache/arcadia/rust/bin, ---config target/linux-cargo.toml and dedicated target. Wayland works with -WGPU_BACKEND=vulkan and VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json. -Optional X11 missing libxkbcommon-x11-0; user has sudo apt-get install command. -Do not bypass authentication. Windows/Wayland/REST are unblocked. - -## Exact next actions - -1. Complete native constructor fixtures and compact process pane acceptance. -2. Operate corrected invalid-local-open and stale catalog on newest Windows build; - complete Linux hosted/semantic native acceptance. -3. Independently review/integrate SEM Annotation/Binding and HOST Home/recents, - then ordered/attachment/artifact/multi-source GUI and recoverable checkpoints. -4. Finish advanced OA/process/scenario, help/coverage and fresh final reviews; - full platforms/MSRV/docs/package verification. No release handoff yet. - -Build: cargo test --locked --offline --workspace; -cargo clippy --locked --offline --workspace --all-targets -- -D warnings; -cargo build --locked --offline --release -p oa-desktop -p oa-server. -Dedicated targets only. No remote push, usage reset or broad target deletion. +Meaningful progress continues: native lifecycle and Linux no-replace uncertainty have been resolved with exact oracles. The highest-value work is current-binary validation and closing uncovered engineering workflows, not accumulating repeated historical headings or treating aggregate test counts as release completion. diff --git a/harness/TASK_LEDGER.md b/harness/TASK_LEDGER.md index 8be25b97..c3537e35 100644 --- a/harness/TASK_LEDGER.md +++ b/harness/TASK_LEDGER.md @@ -1,142 +1,56 @@ # Task Ledger — first release -Authoritative objective: attachment10134df9-bea2-4c02-a820-67b53d6b86f7/ -goal-objective.md, read fully2026-09-06. STATE.md records execution; PLAN.md retains -scope; DECISIONS.md and linked contracts preserve rationale. +Updated 2026-09-06 evening after corrected reverse-transition native acceptance, teaching source/history/reopen and Linux server/HTTP restart passes. [STATE.md](STATE.md) owns current execution/evidence and leases. [PLAN.md](PLAN.md) owns the release plan; [DECISIONS.md](DECISIONS.md) preserves architectural rationale. Full pre-consolidation contents, including superseded requirements interpretations and evidence, remain [archived byte-for-byte](history/2026-09-06-ledgers-before-consolidation/TASK_LEDGER.md). ## Goal and success criteria -System engineers develop and iteratively reconcile OA/SA models through equivalent -graphical, textual and REST workflows. Rust/Iced Windows/Linux, strict reST, -traceability, navigable offline guidance and reproducible deployment. Acceptance -requires native workflows, semantic evaluators and fresh independent review. +The authoritative user objective is `C:\Users\User\.codex\attachments\10b0f701-1743-4de7-9ee1-225920e19649\goal-objective.md`. Deliver a usable, substantially complete OA/SA engineering tool in Rust/Iced on Windows and Linux; equivalent graphical, strict textual/reST and REST workflows; practical artifact traceability; navigable offline guidance; recoverable drafts; reproducible deployment. Completion requires native engineering task evidence and fresh independent end-state review, not implementation or passing tests alone. Later Arcadia layers remain future scope. -## Given facts +## Given facts and constraints -- User prioritizes Capella domain fidelity, hierarchy, typed ports and intuitive - transitions in both directions, without its clutter and navigation friction. -- Direct exchange drawing must avoid large endpoint dropdowns; ports must move - between sides; contextual menus should replace persistent action clutter. -- Modular source must be practical for humans/AI and Git. Text/graph/API parity is - required. Behavior canvases must support editing and preserve drag selection. -- Git/worktrees are authorized. Remote publication is not requested. +- User prioritizes Capella domain fidelity, useful containment/nesting and explicit typed ports, while avoiding clutter, persistent action overload and awkward navigation. Study real domain workflows rather than mechanically copying Capella classes. +- Both-layer transitions and reviewed counterpart reconciliation must be intuitive and precise. Behavior/scenario diagrams must be directly editable and preserve selection/camera/task context. +- Human/AI/Git-friendly modular source, comments/includes, complete graphical/source/API parity and clear rejection are required. One validated model/history boundary must govern every representation. +- User permits autonomous reversible engineering, experiments, independent agents and authorized native work. Root is the sole native operator in this collaboration. Do not run concurrent shared Cargo writers. No remote push, usage reset, broad target deletion or host restart is implied by current task authorization. +- User permits isolated Linux work. WSL remains unavailable on this host; a task-local QEMU core pilot completed and shut down cleanly, and a second bounded server/application gate is active. Historical external worktrees/toolchains are not present on this machine. ## Verified facts -- Original Capella SAB creates FE function input/output endpoints; component ports - are separately Standard/Flow with explicit allocation. Native observations are in - release-capella-workflow-benchmark.md; adopted contracts preserve that distinction. -- Explorer/port discovery, editable routing, nested modular source and coupled REST - bundles have scoped Windows/Linux acceptance. Evidence links are in STATE.md. -- Direct palette connections, four-side port anchors, context menus and source - folders passed real Windows/Linux operation. Passive stacked Canvas fixes native - tooltip overprint; separate Frames did not. See release-port-drawing.md. -- Persisted state editing now passes Windows/Linux native and independent review: - creation, movement, direct transitions, typed forms, reconnection, fold/reopen. - See release-state-editing.md. Scenario/process canvases still need direct editing. -- Ordinary Document commands atomically synchronize source/model; failed writes - retain drafts. Source comments require occurrence/provenance-aware edits, not - whole-model equality or flat reformatting. Independent counterexamples were fixed. -- Accepted-main R3 existing-target reconciliation remains name/description only; - isolated typed core/source/API are implemented, with desktop integration active. R6 data shape - expansion is active implementation, not accepted functionality. -- R6 ab661f2 full suites passed Windows607/Linux610; f5467e5 desktop263 and - strict Clippy/release passed on both platforms. Native Windows data editing, - typed defaults, allocations, recursive qualified Call/Reply values, history and - saved reopening passed. Source3 migration now preserves comments and nesting. - Native found flat property browsing still ambiguous; one-record browsing with - bounded pages passes264 desktop tests/Clippy/release on both platforms and native - Windows compact/normal browsing, expression retention and saved reopen. - Linux native R6 acceptance remains outstanding. +- Branch/patch/tree audit established that direct state/scenario/process, data contracts, hosted editing and earlier semantic review are already integrated; no required production transplant was found. [Branch evidence](release-branch-integration-20260906.md). +- Capella's function input/output exchange endpoints and component Standard/Flow ports are distinct; explicit allocation connects them. Preserve that domain distinction. [Native benchmark](release-capella-workflow-benchmark.md). +- Current source-6 implements participant CommunicationMean, typed explicit transition endpoint decisions, saved exchange categories/view selectors, completeness warnings and presentation duplication. Older audit absence claims are superseded; native acceptance remains scoped by reports. +- Semantic review includes independent scalar/constructor/keyed/order/Child/Incident/artifact facets and multiple source rows. One frozen accepted base, explicit mappings, unique merged candidate slots and one atomic command preserve raw excluded/rejected drafts. Recovery replans; it never restores an unchecked serialized command. [Batch design](release-semantic-batch-foundation-design.md), [artifact contract](release-semantic-artifacts.md). +- Native Windows artifact MOVE/CREATE/DELETE passed exact complete Save/Undo/Redo oracles, nine first-attempt saves. MOVE invalid raw optional state and CREATE exclusion/reserved identity survived controlled-stop recovery exactly. Destination-only retention is automated evidence, not separately native-applied in that run. [Native report](release-artifacts-native-20260906.md). +- Native batch, Child/Incident and source-5 checkpoint workflows have scoped exact source/recovery/history evidence. Dense views have persistence/duplicate/hide and bounded readability evidence. Current counts, binaries and remaining cases are in STATE.md; these are not universal usability claims. +- Native communication means drawing, explicit create/reuse ports and reverse Call/Reply pass independent complete model/source and exact Undo/Redo checks. The reverse navigation/overlapping-placement defect is fixed and reverified against a distinct oracle:10newUUIDs,zero implicit placements, correct Behavior owner and preserved Architecture. [Exact evidence](release-communication-native-20260906.md). +- Windows semantic108desktop+2core/full527desktop passes remain scoped. Subsequent guide/home and behavior navigation slices passed focused tests/strict Clippy/build; the latter compiled535desktop tests without claiming a full535run. A separate semantic navigation correction is under current verification. Keep counts scoped until final full-workspace verification. +- Native Scenario ordered review passed ambiguity rejection, explicit retained-local final order and complete Save/Undo/Redo source oracles at1100×700. Current guide binary passes related-topic heading reset, same-topic search scroll retention and Git auto-selected-topic reset without dirtying the model. Branch/fragment order variants and revised artifact diagnostic rendering remain separate gates. [Native ordered evidence](release-current-native-verification.md), [guide/diagnostic implementation](release-semantic-conflict-diagnostics-20260906.md). +- Windows user-local GNU/LLVM builds and isolated clean-PATH native launch work. Actual Ubuntu24.04.4/Rust1.90 core/journal tests pass on hashed snapshots. Actual Linux server build and real HTTP modular import/invalidPUT/cleanrestart exact source/model/ETag pass on the older28-source lesson snapshot. Full server tests are running; current desktop/Linux package remain unverified. [Linux evidence](release-linux-validation-options.md). +- Corrected30-source/37-file teaching project passes minimum-size Home/guide/dirty-source guard, located initial warning, reviewed NameTake and complete exact source/Undo/Redo/reopen checks. The review revealed a separate layer/canvas mismatch; the process workspace is cramped at minimum size. These findings and remaining tutorial tasks are explicit in [native evidence](release-teaching-native-20260906.md). +- Scope reconciliation established two real semantic gaps using concrete OA/SA models: caller-context links into repeated/nested reusable chains (**S1**) and a maintained interface-operation selection for parameterless messages (**S2**). Existing sequence/functional-refinement workarounds change the intended engineering facts. Contracts/probes/independent critiques exist; production additions are pending. [Scope reconciliation](release-scope-reconciliation-20260906.md). + +## Decisions and candidate designs + +Retain three Rust crates, one typed Project and validated atomic Command. Keep semantic ownership, reference/allocation/order relationships and diagram placement separate. Forms remain a closed typed adapter, not a second metamodel. Persistent additions require model validation, references/deletion, transition, parser/updater, API/schema and reachable graphical editing parity. + +Semantic reconciliation uses finite typed facets and separate member/presence/order decisions. Compare actual destination slots before constructing the core's complete candidate; two rows never become sequential Apply. Missing identities are not inferred by names. Raw constructor/numeric/keyed/optional drafts survive rejection, exclusion and checkpoint recovery. Artifact Presence means attachment to the reviewed owner; an identified source moved away can delete the destination, whereas a source key of None preserves destination-only membership. No invented tombstone or silent seed acknowledgment. + +Source updates preserve lexical ownership/comments/includes and exact history. Checkpoint source-5 upgrade admits only the established token/typed migration contract, validates old base fingerprints and resets transient approvals; it does not silently rebase authored drafts. Hosted saves remain explicit and conditional on the resource/ETag; credentials stay in memory. + +For Linux journal publication, ordinary rename lost the empty-target ownership invariant in a real ext4 probe. Selected `renameat2(RENAME_NOREPLACE)` through existing locked libc with no fallback; syscall errors fail before model writes. Alternative overwrite fallback was rejected by that falsifier. Windows native no-replace/retry behavior is retained; other OS behavior is explicit. [Contract, alternatives and measured limits](release-linux-journal-publication.md). + +Root selected **F1**, a complete source-6 teaching-project example. Corrected model/replay tests and bounded native onboarding/review/history pass; the full process/scenario/state/data/evidence lesson remains open. **F2**, faithful behavior-canvas SVG export, is deferred: current export scope is architecture-only, not a claim of faithful scenario/process/state export. Record this as an explicit scope decision rather than an accepted behavior-export result. + +Behavior-root transition Apply should focus the explicitly requested counterpart and its nearest behavior owner. Without an explicit destination diagram it must preserve unrelated architecture presentation. Ordinary architecture transitions retain their existing path; explicit destination remains honored. This decision follows the independently reproduced reverse scenario placement/navigation failure, not a semantic change to the core transition. ## Assumptions and open questions -- Nested aggregate files reduce cognitive load; any additional port/exchange/step - shorthand must justify grammar complexity through real authoring tasks. -- State placements proved viable without a second geometry system. Scenario/chain - gestures still need their own occurrence/order semantics and task verification. -- Routing offers a usable initial path plus manual pins; arbitrary dense diagrams - are not guaranteed crossing-free. Measured dense full solve119ms remains a limit. -- R3 selected design C uses finite self-tagged typed facets, separate membership - and order acknowledgements, and transient constructor groups. Source5 historical - references are implemented in the isolated semantic checkout. Independent review - found narrow-acknowledgement/canonical-agreement/rollup defects; the worker has - added regressions and fixes. Full catalog, GUI/API/spec and acceptance remain. +- Nested source and contextual editors should reduce authoring effort; additional shorthand/grouping must justify grammar or interaction complexity through real tasks. Dense-view path overlap is not itself a failure when every actual identity remains accessible; arbitrary crossing-free layout is not promised. +- Remaining native variants must establish useful engineering workflows: CM transitions, advanced OA process/SA chain/scenario reuse/fragment/order, typed state actions, compact/Linux/hosted cases, and uncovered saved-view lifecycle combinations. Do not expand or shrink required OA/SA scope merely to fit implementation convenience. +- Latest diagnostic/reserved-label corrections have independent review and completed semantic/full-desktop/Clippy/build checks; their new-binary artifact rendering still needs native verification. Latest native process9672/GUIF5ECAE…9246 closed cleanly for the semantic-navigation rebuild; its exact scope is in STATE.md. +- Current Linux evidence includes x86_64/glibc2.39/ext4 core/MSRV and server/HTTP runtime. Full current desktop, packaging and GUI acceptance remain required. Syscall failure tests do not establish power-loss durability, network-filesystem behavior or cross-process model CAS. +- Earlier Windows QA packages include runtime/notices and verified archive contents; final accepted-tree optimized distributions and exact provenance/checksums remain a release gate. [Packaging evidence](release-windows-package-review.md). -## Decisions and current strategy +## Current strategy -Preserve one typed core and validated command boundary. Retain current typed ports; -R6 uses schema/source4 with explicit1/2/3 migration and no intermediate public4 -shape. Freeze payload, defaults, inheritance, associations and identified interface -allocations together. Preserve lexical includes/comments during migration. - -Finish and independently verify R6, then semantic R3 reconciliation and direct -scenario/process gestures. Complete lifecycle/recovery, hosted editing, guidance -and fresh cross-disciplinary Windows/Linux acceptance. No subsystem checkpoint -completes the first release. Current work is making measured implementation, -review and runtime progress. User reauthorized Computer Use. Root resumed combined Windows scenario tests; -middle insertion, history, save, reorder and scroll/Escape cancellation passed. -Three native UI findings are being refined; Linux acceptance remains pending. - - -Latest verified facts: integrated60d162c passes306 desktop tests/strict Clippy/build -on Windows and Linux. Windows ALT/pinned-actions/viewport retention native passed; -Linux native is active. RESTd130013 passed48 server tests plus served/local schema -validation and independent review after direct user confirmation resolved approval. -New constructor-acknowledgment reachability defect was independently reproduced; -narrow core fix and separate scalar GUI implementation are active in SEM. These -are current unresolved work, not accepted release limitations. See current STATE. - -## Current evidence superseding earlier in-progress notes — 2026-09-06 - -Semantic source5/core/REST and scalar GUI are integrated at d114a5d. Full Windows718/ -Linux721 tests, strict Clippy, optimized builds and live semantic REST contracts pass. -The equal initial discriminator acknowledgment defect is fixed and independently -closed. Three scalar GUI P2 fixes5a51a75 are under fresh recheck/integration; structured -authoring remains required. Basic Linux R6, middle scenario and guarded chain insertion -have actual native evidence in their ledgers; advanced and compact cases remain. -X11 lacks libxkbcommon-x11-0, installation requires user's sudo password. User has -the exact fix command; Windows/Wayland progress is independent. This environment -issue is not evidence of a semantic defect or reason to mark the release blocked. - -2026-09-06: DATA67ed8e0/Linux43dd566 integrates bounded MessageKind/TimerExpression -review.335 desktop tests and strict checks/build pass per platform; independent22 -module tests+288 differential cases close no findings. Windows native scalar now -includes named mapping/default, historical identities, reopen and reverse Undo; -trial closed0/empty stderr. Exact scope and pending native cases: -release-semantic-native.md. Hosted desktop implementation is isolated in -Capella2-hosted-desktop; all release completion claims remain pending. - -## Hosted/native and constructor checkpoint — 2026-09-06 - -Verified: optional hosted desktop is integrated; native publish, exact authored -bundle preservation, graphical/text Save, invalid-source rejection, API artifact -review/reload/Undo/Redo and competing-revision local retention passed on Windows. -Minimum hosted dialog controls remain reachable. Offline/reopen/local Save As and -Linux native hosted acceptance continue. No external server was deployed. - -Independent core review closed43370de/8a75294 with31 semantic tests, optional -Binding/Annotation both-direction probes,3 reverse Port boundary and13 port -regressions. Full owner core suite359 passes. Semantics preserve target-local keyed -members without resurrecting source removals, mandatory reset and OA Legacy rules. -Remaining constructor GUIc149f2f passes299 SEM tests; fresh review is active. - -Native process camera/owner fix acceptance passed. New compact form finding was -fixed and passed real Iced layout+360 desktop tests; native fix acceptance pending. -Strategy remains measured release completion, not treating these slices as full -source/UI/API parity. Keyed/order/reset, artifacts/multi-source, remaining advanced -native cases, Home/recovery/help/coverage and final acceptance are still required. - -## Reviewed corrections and native Data evidence — 2026-09-06 - -Verified: constructorc149f2f/reset-unordered6fb01ae and hostedb04c4b3/cataloge807086 -are independently reviewed/integrated. Both platforms pass392 desktop tests and -strict gates. Native Data Take matched the whole core-generated Project/history; -Candidate raw invalid/type mismatch/repair/compact Apply/Undo/Redo/Save passed. -Candidate exactProject and remaining constructors/Linux native are still pending. -Strategy: finish structured parity while Home/recents progresses independently; -recovery, ordered/attachments/artifacts/multi-source and final acceptance remain. - - -## 2026-09-06 branch reconciliation -Verified integrated baseline c091f80 contains direct state/scenario/process editing, data contracts, schema 5 semantic review and hosted editing. Current branch codex/release1-continuation adopts it; independent ancestry/patch/tree audits found no required production transplant. Historical environment is absent here. Current user objective is attachment 10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md. See release-branch-integration-20260906.md for evidence and ownership. Assumption: portable user-local GNU tooling can verify Windows without system installation; being tested, not yet established. +Finish the semantic-review navigation correction and native replay. Freeze the independently critiqued S1/S2 contracts together, including exact old5/6 checkpoint/message compatibility, before coordinated source7/core implementation and graphical/source/REST parity. Complete the remaining tutorial/artifact/view/process/state/hosted workflows while bounded Linux validation progresses. Then run full-workspace, complete Linux and optimized/MSRV/REST/docs/package gates and fresh independent end-state review. Preserve exact revision/binary/platform scope; successful slices and architecture-only SVG export do not complete the broader goal. diff --git a/harness/fixtures/interface-operation-probe/README.md b/harness/fixtures/interface-operation-probe/README.md new file mode 100644 index 00000000..8d56c55f --- /dev/null +++ b/harness/fixtures/interface-operation-probe/README.md @@ -0,0 +1,41 @@ +# Existing-core interface-operation expressivity probe + +`probe.rs` is a standalone executable using the unchanged shared core. It writes +three deterministic complete model/source snapshots under a supplied directory. +There is no proposed DTO implemented in this helper and no native input. + +Observed result, 2026-09-06: + +- Valid baseline:12 elements/5 relations, one System/Actor pair, two Standard + ports, one boundary/interface, two Synchronous parameterless Operation items, + two participant lifelines and two Calls. +- Both Calls' typed bodies are identical. Their names express different intent; + neither Call references its intended item/allocation identity. +- Using item or allocation as the existing exchange reference rejects with + `occurrence_exchange`; adding an `operation` field to the typed JSON rejects as + unknown. No undiscovered existing selector is being relabeled missing. +- Renaming then deleting Reset preserves the Call body and leaves the Call + structurally valid, because there is no maintained operation-use reference. +- Explicit functional refinement distinguishes the two messages and preserves the + same boundary, but adds6 elements (two Functions/four ports) and10 relationships. + It is a real alternative when that refinement is intended, not a zero-cost + encoding of the unrefined interface-only model. +- All three output models pass complete source and JSON round trips. No core or + desktop production file was changed. + +The first helper compile lacked explicit validation imports; initial construction +also used the disabled generic Port factory and reversed Allocation/PortAllocation +directions. Those helper errors were corrected before the successful full run. +They are not product defects. Final compilation used `-D warnings` and succeeded. + +Reproduce only after coordinating stable rlibs/shared resources: + +```powershell +. .local/toolchain/env.ps1 +rustc --edition 2024 -D warnings -C linker=clang.exe --extern oa_core=.local/target-gnullvm/debug/deps/liboa_core-6c8da57367eec642.rlib --extern serde_json=.local/target-gnullvm/debug/deps/libserde_json-da2c4f333fc47818.rlib -L dependency=.local/target-gnullvm/debug/deps harness/fixtures/interface-operation-probe/probe.rs -o .local/native-fixtures/interface-operation-probe.exe +.local/native-fixtures/interface-operation-probe.exe NEW_OUTPUT_DIRECTORY +``` + +Final output is retained in `.local/native-fixtures/interface-operation-probe.log`. +The tracked `output/` files are probe oracles, not native acceptance fixtures. +See [the proposed contract](../../release-interface-operation-contract-20260906.md). diff --git a/harness/fixtures/interface-operation-probe/hashes.json b/harness/fixtures/interface-operation-probe/hashes.json new file mode 100644 index 00000000..3beac81b --- /dev/null +++ b/harness/fixtures/interface-operation-probe/hashes.json @@ -0,0 +1,14 @@ +{ + "algorithm": "SHA256", + "core_rlib": "e53a68018ab470f3d1e3c024a06fc0b5f1618d1495e6095716e9c4d9105680e0", + "serde_json_rlib": "6fd0f5ebc7956d12d0828f218368abdb366f73e075fbe745259ba7f736be1f3f", + "files": { + "output/after-reset-deletion.arcadia": "c0fe9570684c0061efc4cad65e339fede6961b04ca90c6f324c787bd65e61226", + "output/after-reset-deletion.json": "93187f7a1b476a44185725c6b15f2e1767482b6911ab1280144348d033eb51f0", + "output/boundary-only.arcadia": "02f85e39957ebe6d56336fa729a5c8b10f51865face9d3b2c9679200571fcfc7", + "output/boundary-only.json": "756f9710336e6734ce3592ca774c6f104bba5a37c9e165540eb39326682393d1", + "output/explicit-functional-refinement.arcadia": "ac5b629ba5596464f9f8a975b8bc2764e3007ceb7342de3f828d4b18e4f069b7", + "output/explicit-functional-refinement.json": "78a47c978dc667aa1c45af711f06aa2a0d3c581003013e30c3e02fb0faae2e64", + "probe.rs": "72b1b443e25eb20420dec4477b0fb5ef5d1c705e3882c81df88dd578ab96a426" + } +} diff --git a/harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia b/harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia new file mode 100644 index 00000000..b64fd1f8 --- /dev/null +++ b/harness/fixtures/interface-operation-probe/output/after-reset-deletion.arcadia @@ -0,0 +1,138 @@ +arcadia 6; +project mission "Two operations, one interface" id "52060906-0000-4000-8000-000000000001" { + description ""; + + sa_actor operation_probe_10 "Operator console" id "52060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + sa_system operation_probe_11 "Valve service" id "52060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + sa_port operation_probe_12 "Service socket" id "52060906-0000-4000-8000-00000000000c" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @operation_probe_10; + } + + sa_port operation_probe_13 "Service socket" id "52060906-0000-4000-8000-00000000000d" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @operation_probe_11; + } + + sa_exchange_item operation_probe_21 "ReadStatus" id "52060906-0000-4000-8000-000000000015" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; + } + + sa_interface operation_probe_22 "Valve commands" id "52060906-0000-4000-8000-000000000016" { + description ""; + parent none; + supertype none; + } + + sa_scenario operation_probe_40 "Reset then read status" id "52060906-0000-4000-8000-000000000028" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@operation_probe_43, @operation_probe_44]; + viewpoint BoundaryInterface; + } + + sa_lifeline operation_probe_41 "Service participant" id "52060906-0000-4000-8000-000000000029" { + description ""; + parent @operation_probe_40; + represented @operation_probe_10; + } + + sa_lifeline operation_probe_42 "Service participant" id "52060906-0000-4000-8000-00000000002a" { + description ""; + parent @operation_probe_40; + represented @operation_probe_11; + } + + sa_occurrence operation_probe_43 "Call Reset" id "52060906-0000-4000-8000-00000000002b" { + body { + Exchange { + exchange @operation_probe_34; + message_kind Call; + receiver @operation_probe_42; + reply_to none; + sender @operation_probe_41; + values []; + }; + }; + description ""; + parent @operation_probe_40; + postcondition ""; + precondition ""; + } + + sa_occurrence operation_probe_44 "Call ReadStatus" id "52060906-0000-4000-8000-00000000002c" { + body { + Exchange { + exchange @operation_probe_34; + message_kind Call; + receiver @operation_probe_42; + reply_to none; + sender @operation_probe_41; + values []; + }; + }; + description ""; + parent @operation_probe_40; + postcondition ""; + precondition ""; + } + + interface_item_allocation operation_probe_31 "InterfaceItemAllocation 31" id "52060906-0000-4000-8000-00000000001f" { + acquisition Unspecified; + description ""; + order 0; + source @operation_probe_22; + target @operation_probe_21; + transmission Synchronous; + } + + interface_requirement operation_probe_32 "InterfaceRequirement 32" id "52060906-0000-4000-8000-000000000020" { + description ""; + source @operation_probe_12; + target @operation_probe_22; + } + + interface_provision operation_probe_33 "InterfaceProvision 33" id "52060906-0000-4000-8000-000000000021" { + description ""; + source @operation_probe_13; + target @operation_probe_22; + } + + boundary_exchange operation_probe_34 "BoundaryExchange 34" id "52060906-0000-4000-8000-000000000022" { + description ""; + items [@operation_probe_21]; + oriented false; + source @operation_probe_12; + target @operation_probe_13; + } + +} diff --git a/harness/fixtures/interface-operation-probe/output/after-reset-deletion.json b/harness/fixtures/interface-operation-probe/output/after-reset-deletion.json new file mode 100644 index 00000000..5603b37c --- /dev/null +++ b/harness/fixtures/interface-operation-probe/output/after-reset-deletion.json @@ -0,0 +1,276 @@ +{ + "schema_version": 6, + "id": "52060906-0000-4000-8000-000000000001", + "alias": "mission", + "name": "Two operations, one interface", + "description": "", + "elements": { + "52060906-0000-4000-8000-00000000000a": { + "id": "52060906-0000-4000-8000-00000000000a", + "alias": "operation_probe_10", + "name": "Operator console", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "52060906-0000-4000-8000-00000000000b": { + "id": "52060906-0000-4000-8000-00000000000b", + "alias": "operation_probe_11", + "name": "Valve service", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "52060906-0000-4000-8000-00000000000c": { + "id": "52060906-0000-4000-8000-00000000000c", + "alias": "operation_probe_12", + "name": "Service socket", + "description": "", + "parent": "52060906-0000-4000-8000-00000000000a", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-00000000000d": { + "id": "52060906-0000-4000-8000-00000000000d", + "alias": "operation_probe_13", + "name": "Service socket", + "description": "", + "parent": "52060906-0000-4000-8000-00000000000b", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000015": { + "id": "52060906-0000-4000-8000-000000000015", + "alias": "operation_probe_21", + "name": "ReadStatus", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000016": { + "id": "52060906-0000-4000-8000-000000000016", + "alias": "operation_probe_22", + "name": "Valve commands", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Interface": { + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000028": { + "id": "52060906-0000-4000-8000-000000000028", + "alias": "operation_probe_40", + "name": "Reset then read status", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Scenario": { + "viewpoint": "BoundaryInterface", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "52060906-0000-4000-8000-00000000002b", + "52060906-0000-4000-8000-00000000002c" + ] + } + } + } + } + }, + "52060906-0000-4000-8000-000000000029": { + "id": "52060906-0000-4000-8000-000000000029", + "alias": "operation_probe_41", + "name": "Service participant", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "52060906-0000-4000-8000-00000000000a" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002a": { + "id": "52060906-0000-4000-8000-00000000002a", + "alias": "operation_probe_42", + "name": "Service participant", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "52060906-0000-4000-8000-00000000000b" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002b": { + "id": "52060906-0000-4000-8000-00000000002b", + "alias": "operation_probe_43", + "name": "Call Reset", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "52060906-0000-4000-8000-000000000022", + "sender": "52060906-0000-4000-8000-000000000029", + "receiver": "52060906-0000-4000-8000-00000000002a", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002c": { + "id": "52060906-0000-4000-8000-00000000002c", + "alias": "operation_probe_44", + "name": "Call ReadStatus", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "52060906-0000-4000-8000-000000000022", + "sender": "52060906-0000-4000-8000-000000000029", + "receiver": "52060906-0000-4000-8000-00000000002a", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + } + }, + "relations": { + "52060906-0000-4000-8000-00000000001f": { + "id": "52060906-0000-4000-8000-00000000001f", + "alias": "operation_probe_31", + "name": "InterfaceItemAllocation 31", + "description": "", + "body": { + "InterfaceItemAllocation": { + "source": "52060906-0000-4000-8000-000000000016", + "target": "52060906-0000-4000-8000-000000000015", + "order": 0, + "transmission": "Synchronous", + "acquisition": "Unspecified" + } + } + }, + "52060906-0000-4000-8000-000000000020": { + "id": "52060906-0000-4000-8000-000000000020", + "alias": "operation_probe_32", + "name": "InterfaceRequirement 32", + "description": "", + "body": { + "InterfaceRequirement": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-000000000016" + } + } + }, + "52060906-0000-4000-8000-000000000021": { + "id": "52060906-0000-4000-8000-000000000021", + "alias": "operation_probe_33", + "name": "InterfaceProvision 33", + "description": "", + "body": { + "InterfaceProvision": { + "source": "52060906-0000-4000-8000-00000000000d", + "target": "52060906-0000-4000-8000-000000000016" + } + } + }, + "52060906-0000-4000-8000-000000000022": { + "id": "52060906-0000-4000-8000-000000000022", + "alias": "operation_probe_34", + "name": "BoundaryExchange 34", + "description": "", + "body": { + "BoundaryExchange": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-00000000000d", + "oriented": false, + "items": [ + "52060906-0000-4000-8000-000000000015" + ] + } + } + } + }, + "diagrams": {}, + "artifact_links": {}, + "traces": {} +} \ No newline at end of file diff --git a/harness/fixtures/interface-operation-probe/output/boundary-only.arcadia b/harness/fixtures/interface-operation-probe/output/boundary-only.arcadia new file mode 100644 index 00000000..1afb0223 --- /dev/null +++ b/harness/fixtures/interface-operation-probe/output/boundary-only.arcadia @@ -0,0 +1,155 @@ +arcadia 6; +project mission "Two operations, one interface" id "52060906-0000-4000-8000-000000000001" { + description ""; + + sa_actor operation_probe_10 "Operator console" id "52060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + sa_system operation_probe_11 "Valve service" id "52060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + sa_port operation_probe_12 "Service socket" id "52060906-0000-4000-8000-00000000000c" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @operation_probe_10; + } + + sa_port operation_probe_13 "Service socket" id "52060906-0000-4000-8000-00000000000d" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @operation_probe_11; + } + + sa_exchange_item operation_probe_20 "Reset" id "52060906-0000-4000-8000-000000000014" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; + } + + sa_exchange_item operation_probe_21 "ReadStatus" id "52060906-0000-4000-8000-000000000015" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; + } + + sa_interface operation_probe_22 "Valve commands" id "52060906-0000-4000-8000-000000000016" { + description ""; + parent none; + supertype none; + } + + sa_scenario operation_probe_40 "Reset then read status" id "52060906-0000-4000-8000-000000000028" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@operation_probe_43, @operation_probe_44]; + viewpoint BoundaryInterface; + } + + sa_lifeline operation_probe_41 "Service participant" id "52060906-0000-4000-8000-000000000029" { + description ""; + parent @operation_probe_40; + represented @operation_probe_10; + } + + sa_lifeline operation_probe_42 "Service participant" id "52060906-0000-4000-8000-00000000002a" { + description ""; + parent @operation_probe_40; + represented @operation_probe_11; + } + + sa_occurrence operation_probe_43 "Call Reset" id "52060906-0000-4000-8000-00000000002b" { + body { + Exchange { + exchange @operation_probe_34; + message_kind Call; + receiver @operation_probe_42; + reply_to none; + sender @operation_probe_41; + values []; + }; + }; + description ""; + parent @operation_probe_40; + postcondition ""; + precondition ""; + } + + sa_occurrence operation_probe_44 "Call ReadStatus" id "52060906-0000-4000-8000-00000000002c" { + body { + Exchange { + exchange @operation_probe_34; + message_kind Call; + receiver @operation_probe_42; + reply_to none; + sender @operation_probe_41; + values []; + }; + }; + description ""; + parent @operation_probe_40; + postcondition ""; + precondition ""; + } + + interface_item_allocation operation_probe_30 "InterfaceItemAllocation 30" id "52060906-0000-4000-8000-00000000001e" { + acquisition Unspecified; + description ""; + order 0; + source @operation_probe_22; + target @operation_probe_20; + transmission Synchronous; + } + + interface_item_allocation operation_probe_31 "InterfaceItemAllocation 31" id "52060906-0000-4000-8000-00000000001f" { + acquisition Unspecified; + description ""; + order 0; + source @operation_probe_22; + target @operation_probe_21; + transmission Synchronous; + } + + interface_requirement operation_probe_32 "InterfaceRequirement 32" id "52060906-0000-4000-8000-000000000020" { + description ""; + source @operation_probe_12; + target @operation_probe_22; + } + + interface_provision operation_probe_33 "InterfaceProvision 33" id "52060906-0000-4000-8000-000000000021" { + description ""; + source @operation_probe_13; + target @operation_probe_22; + } + + boundary_exchange operation_probe_34 "BoundaryExchange 34" id "52060906-0000-4000-8000-000000000022" { + description ""; + items [@operation_probe_20, @operation_probe_21]; + oriented false; + source @operation_probe_12; + target @operation_probe_13; + } + +} diff --git a/harness/fixtures/interface-operation-probe/output/boundary-only.json b/harness/fixtures/interface-operation-probe/output/boundary-only.json new file mode 100644 index 00000000..72f13ed7 --- /dev/null +++ b/harness/fixtures/interface-operation-probe/output/boundary-only.json @@ -0,0 +1,310 @@ +{ + "schema_version": 6, + "id": "52060906-0000-4000-8000-000000000001", + "alias": "mission", + "name": "Two operations, one interface", + "description": "", + "elements": { + "52060906-0000-4000-8000-00000000000a": { + "id": "52060906-0000-4000-8000-00000000000a", + "alias": "operation_probe_10", + "name": "Operator console", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "52060906-0000-4000-8000-00000000000b": { + "id": "52060906-0000-4000-8000-00000000000b", + "alias": "operation_probe_11", + "name": "Valve service", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "52060906-0000-4000-8000-00000000000c": { + "id": "52060906-0000-4000-8000-00000000000c", + "alias": "operation_probe_12", + "name": "Service socket", + "description": "", + "parent": "52060906-0000-4000-8000-00000000000a", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-00000000000d": { + "id": "52060906-0000-4000-8000-00000000000d", + "alias": "operation_probe_13", + "name": "Service socket", + "description": "", + "parent": "52060906-0000-4000-8000-00000000000b", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000014": { + "id": "52060906-0000-4000-8000-000000000014", + "alias": "operation_probe_20", + "name": "Reset", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000015": { + "id": "52060906-0000-4000-8000-000000000015", + "alias": "operation_probe_21", + "name": "ReadStatus", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000016": { + "id": "52060906-0000-4000-8000-000000000016", + "alias": "operation_probe_22", + "name": "Valve commands", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Interface": { + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000028": { + "id": "52060906-0000-4000-8000-000000000028", + "alias": "operation_probe_40", + "name": "Reset then read status", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Scenario": { + "viewpoint": "BoundaryInterface", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "52060906-0000-4000-8000-00000000002b", + "52060906-0000-4000-8000-00000000002c" + ] + } + } + } + } + }, + "52060906-0000-4000-8000-000000000029": { + "id": "52060906-0000-4000-8000-000000000029", + "alias": "operation_probe_41", + "name": "Service participant", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "52060906-0000-4000-8000-00000000000a" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002a": { + "id": "52060906-0000-4000-8000-00000000002a", + "alias": "operation_probe_42", + "name": "Service participant", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "52060906-0000-4000-8000-00000000000b" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002b": { + "id": "52060906-0000-4000-8000-00000000002b", + "alias": "operation_probe_43", + "name": "Call Reset", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "52060906-0000-4000-8000-000000000022", + "sender": "52060906-0000-4000-8000-000000000029", + "receiver": "52060906-0000-4000-8000-00000000002a", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002c": { + "id": "52060906-0000-4000-8000-00000000002c", + "alias": "operation_probe_44", + "name": "Call ReadStatus", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "52060906-0000-4000-8000-000000000022", + "sender": "52060906-0000-4000-8000-000000000029", + "receiver": "52060906-0000-4000-8000-00000000002a", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + } + }, + "relations": { + "52060906-0000-4000-8000-00000000001e": { + "id": "52060906-0000-4000-8000-00000000001e", + "alias": "operation_probe_30", + "name": "InterfaceItemAllocation 30", + "description": "", + "body": { + "InterfaceItemAllocation": { + "source": "52060906-0000-4000-8000-000000000016", + "target": "52060906-0000-4000-8000-000000000014", + "order": 0, + "transmission": "Synchronous", + "acquisition": "Unspecified" + } + } + }, + "52060906-0000-4000-8000-00000000001f": { + "id": "52060906-0000-4000-8000-00000000001f", + "alias": "operation_probe_31", + "name": "InterfaceItemAllocation 31", + "description": "", + "body": { + "InterfaceItemAllocation": { + "source": "52060906-0000-4000-8000-000000000016", + "target": "52060906-0000-4000-8000-000000000015", + "order": 0, + "transmission": "Synchronous", + "acquisition": "Unspecified" + } + } + }, + "52060906-0000-4000-8000-000000000020": { + "id": "52060906-0000-4000-8000-000000000020", + "alias": "operation_probe_32", + "name": "InterfaceRequirement 32", + "description": "", + "body": { + "InterfaceRequirement": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-000000000016" + } + } + }, + "52060906-0000-4000-8000-000000000021": { + "id": "52060906-0000-4000-8000-000000000021", + "alias": "operation_probe_33", + "name": "InterfaceProvision 33", + "description": "", + "body": { + "InterfaceProvision": { + "source": "52060906-0000-4000-8000-00000000000d", + "target": "52060906-0000-4000-8000-000000000016" + } + } + }, + "52060906-0000-4000-8000-000000000022": { + "id": "52060906-0000-4000-8000-000000000022", + "alias": "operation_probe_34", + "name": "BoundaryExchange 34", + "description": "", + "body": { + "BoundaryExchange": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-00000000000d", + "oriented": false, + "items": [ + "52060906-0000-4000-8000-000000000014", + "52060906-0000-4000-8000-000000000015" + ] + } + } + } + }, + "diagrams": {}, + "artifact_links": {}, + "traces": {} +} \ No newline at end of file diff --git a/harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia b/harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia new file mode 100644 index 00000000..f18bac6c --- /dev/null +++ b/harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.arcadia @@ -0,0 +1,275 @@ +arcadia 6; +project mission "Two operations, one interface" id "52060906-0000-4000-8000-000000000001" { + description ""; + + sa_actor operation_probe_10 "Operator console" id "52060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + sa_system operation_probe_11 "Valve service" id "52060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + sa_port operation_probe_12 "Service socket" id "52060906-0000-4000-8000-00000000000c" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @operation_probe_10; + } + + sa_port operation_probe_13 "Service socket" id "52060906-0000-4000-8000-00000000000d" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @operation_probe_11; + } + + sa_exchange_item operation_probe_20 "Reset" id "52060906-0000-4000-8000-000000000014" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; + } + + sa_exchange_item operation_probe_21 "ReadStatus" id "52060906-0000-4000-8000-000000000015" { + description ""; + kind Information; + mechanism Operation; + parent none; + supertype none; + } + + sa_interface operation_probe_22 "Valve commands" id "52060906-0000-4000-8000-000000000016" { + description ""; + parent none; + supertype none; + } + + sa_scenario operation_probe_40 "Reset then read status" id "52060906-0000-4000-8000-000000000028" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@operation_probe_43, @operation_probe_44]; + viewpoint BoundaryInterface; + } + + sa_lifeline operation_probe_41 "Service participant" id "52060906-0000-4000-8000-000000000029" { + description ""; + parent @operation_probe_40; + represented @operation_probe_10; + } + + sa_lifeline operation_probe_42 "Service participant" id "52060906-0000-4000-8000-00000000002a" { + description ""; + parent @operation_probe_40; + represented @operation_probe_11; + } + + sa_occurrence operation_probe_43 "Call Reset" id "52060906-0000-4000-8000-00000000002b" { + body { + Exchange { + exchange @operation_probe_80; + message_kind Call; + receiver @operation_probe_42; + reply_to none; + sender @operation_probe_41; + values []; + }; + }; + description ""; + parent @operation_probe_40; + postcondition ""; + precondition ""; + } + + sa_occurrence operation_probe_44 "Call ReadStatus" id "52060906-0000-4000-8000-00000000002c" { + body { + Exchange { + exchange @operation_probe_81; + message_kind Call; + receiver @operation_probe_42; + reply_to none; + sender @operation_probe_41; + values []; + }; + }; + description ""; + parent @operation_probe_40; + postcondition ""; + precondition ""; + } + + sa_function operation_probe_50 "Issue service request" id "52060906-0000-4000-8000-000000000032" { + description ""; + parent none; + } + + sa_function operation_probe_51 "Execute service request" id "52060906-0000-4000-8000-000000000033" { + description ""; + parent none; + } + + sa_port operation_probe_70 "Operation output" id "52060906-0000-4000-8000-000000000046" { + contract { + FunctionOutput { + outgoing_items [@operation_probe_20]; + }; + }; + description ""; + legacy none; + parent @operation_probe_50; + } + + sa_port operation_probe_71 "Operation input" id "52060906-0000-4000-8000-000000000047" { + contract { + FunctionInput { + incoming_items [@operation_probe_20]; + }; + }; + description ""; + legacy none; + parent @operation_probe_51; + } + + sa_port operation_probe_72 "Operation output" id "52060906-0000-4000-8000-000000000048" { + contract { + FunctionOutput { + outgoing_items [@operation_probe_21]; + }; + }; + description ""; + legacy none; + parent @operation_probe_50; + } + + sa_port operation_probe_73 "Operation input" id "52060906-0000-4000-8000-000000000049" { + contract { + FunctionInput { + incoming_items [@operation_probe_21]; + }; + }; + description ""; + legacy none; + parent @operation_probe_51; + } + + interface_item_allocation operation_probe_30 "InterfaceItemAllocation 30" id "52060906-0000-4000-8000-00000000001e" { + acquisition Unspecified; + description ""; + order 0; + source @operation_probe_22; + target @operation_probe_20; + transmission Synchronous; + } + + interface_item_allocation operation_probe_31 "InterfaceItemAllocation 31" id "52060906-0000-4000-8000-00000000001f" { + acquisition Unspecified; + description ""; + order 0; + source @operation_probe_22; + target @operation_probe_21; + transmission Synchronous; + } + + interface_requirement operation_probe_32 "InterfaceRequirement 32" id "52060906-0000-4000-8000-000000000020" { + description ""; + source @operation_probe_12; + target @operation_probe_22; + } + + interface_provision operation_probe_33 "InterfaceProvision 33" id "52060906-0000-4000-8000-000000000021" { + description ""; + source @operation_probe_13; + target @operation_probe_22; + } + + boundary_exchange operation_probe_34 "BoundaryExchange 34" id "52060906-0000-4000-8000-000000000022" { + description ""; + items [@operation_probe_20, @operation_probe_21]; + oriented false; + source @operation_probe_12; + target @operation_probe_13; + } + + allocation operation_probe_60 "Allocation 60" id "52060906-0000-4000-8000-00000000003c" { + description ""; + source @operation_probe_10; + target @operation_probe_50; + } + + allocation operation_probe_61 "Allocation 61" id "52060906-0000-4000-8000-00000000003d" { + description ""; + source @operation_probe_11; + target @operation_probe_51; + } + + functional_exchange operation_probe_80 "FunctionalExchange 80" id "52060906-0000-4000-8000-000000000050" { + description ""; + items [@operation_probe_20]; + source @operation_probe_50; + source_port @operation_probe_70; + target @operation_probe_51; + target_port @operation_probe_71; + } + + functional_exchange operation_probe_81 "FunctionalExchange 81" id "52060906-0000-4000-8000-000000000051" { + description ""; + items [@operation_probe_21]; + source @operation_probe_50; + source_port @operation_probe_72; + target @operation_probe_51; + target_port @operation_probe_73; + } + + port_allocation operation_probe_90 "PortAllocation 90" id "52060906-0000-4000-8000-00000000005a" { + description ""; + source @operation_probe_12; + target @operation_probe_70; + } + + port_allocation operation_probe_91 "PortAllocation 91" id "52060906-0000-4000-8000-00000000005b" { + description ""; + source @operation_probe_13; + target @operation_probe_71; + } + + port_allocation operation_probe_92 "PortAllocation 92" id "52060906-0000-4000-8000-00000000005c" { + description ""; + source @operation_probe_12; + target @operation_probe_72; + } + + port_allocation operation_probe_93 "PortAllocation 93" id "52060906-0000-4000-8000-00000000005d" { + description ""; + source @operation_probe_13; + target @operation_probe_73; + } + + exchange_allocation operation_probe_100 "ExchangeAllocation 100" id "52060906-0000-4000-8000-000000000064" { + description ""; + source @operation_probe_34; + target @operation_probe_80; + } + + exchange_allocation operation_probe_101 "ExchangeAllocation 101" id "52060906-0000-4000-8000-000000000065" { + description ""; + source @operation_probe_34; + target @operation_probe_81; + } + +} diff --git a/harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json b/harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json new file mode 100644 index 00000000..f75e8c0a --- /dev/null +++ b/harness/fixtures/interface-operation-probe/output/explicit-functional-refinement.json @@ -0,0 +1,552 @@ +{ + "schema_version": 6, + "id": "52060906-0000-4000-8000-000000000001", + "alias": "mission", + "name": "Two operations, one interface", + "description": "", + "elements": { + "52060906-0000-4000-8000-00000000000a": { + "id": "52060906-0000-4000-8000-00000000000a", + "alias": "operation_probe_10", + "name": "Operator console", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "52060906-0000-4000-8000-00000000000b": { + "id": "52060906-0000-4000-8000-00000000000b", + "alias": "operation_probe_11", + "name": "Valve service", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "52060906-0000-4000-8000-00000000000c": { + "id": "52060906-0000-4000-8000-00000000000c", + "alias": "operation_probe_12", + "name": "Service socket", + "description": "", + "parent": "52060906-0000-4000-8000-00000000000a", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-00000000000d": { + "id": "52060906-0000-4000-8000-00000000000d", + "alias": "operation_probe_13", + "name": "Service socket", + "description": "", + "parent": "52060906-0000-4000-8000-00000000000b", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000014": { + "id": "52060906-0000-4000-8000-000000000014", + "alias": "operation_probe_20", + "name": "Reset", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000015": { + "id": "52060906-0000-4000-8000-000000000015", + "alias": "operation_probe_21", + "name": "ReadStatus", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Operation", + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000016": { + "id": "52060906-0000-4000-8000-000000000016", + "alias": "operation_probe_22", + "name": "Valve commands", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Interface": { + "supertype": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000028": { + "id": "52060906-0000-4000-8000-000000000028", + "alias": "operation_probe_40", + "name": "Reset then read status", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Scenario": { + "viewpoint": "BoundaryInterface", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "52060906-0000-4000-8000-00000000002b", + "52060906-0000-4000-8000-00000000002c" + ] + } + } + } + } + }, + "52060906-0000-4000-8000-000000000029": { + "id": "52060906-0000-4000-8000-000000000029", + "alias": "operation_probe_41", + "name": "Service participant", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "52060906-0000-4000-8000-00000000000a" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002a": { + "id": "52060906-0000-4000-8000-00000000002a", + "alias": "operation_probe_42", + "name": "Service participant", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "52060906-0000-4000-8000-00000000000b" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002b": { + "id": "52060906-0000-4000-8000-00000000002b", + "alias": "operation_probe_43", + "name": "Call Reset", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "52060906-0000-4000-8000-000000000050", + "sender": "52060906-0000-4000-8000-000000000029", + "receiver": "52060906-0000-4000-8000-00000000002a", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "52060906-0000-4000-8000-00000000002c": { + "id": "52060906-0000-4000-8000-00000000002c", + "alias": "operation_probe_44", + "name": "Call ReadStatus", + "description": "", + "parent": "52060906-0000-4000-8000-000000000028", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "52060906-0000-4000-8000-000000000051", + "sender": "52060906-0000-4000-8000-000000000029", + "receiver": "52060906-0000-4000-8000-00000000002a", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "52060906-0000-4000-8000-000000000032": { + "id": "52060906-0000-4000-8000-000000000032", + "alias": "operation_probe_50", + "name": "Issue service request", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "52060906-0000-4000-8000-000000000033": { + "id": "52060906-0000-4000-8000-000000000033", + "alias": "operation_probe_51", + "name": "Execute service request", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "52060906-0000-4000-8000-000000000046": { + "id": "52060906-0000-4000-8000-000000000046", + "alias": "operation_probe_70", + "name": "Operation output", + "description": "", + "parent": "52060906-0000-4000-8000-000000000032", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionOutput": { + "outgoing_items": [ + "52060906-0000-4000-8000-000000000014" + ] + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000047": { + "id": "52060906-0000-4000-8000-000000000047", + "alias": "operation_probe_71", + "name": "Operation input", + "description": "", + "parent": "52060906-0000-4000-8000-000000000033", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionInput": { + "incoming_items": [ + "52060906-0000-4000-8000-000000000014" + ] + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000048": { + "id": "52060906-0000-4000-8000-000000000048", + "alias": "operation_probe_72", + "name": "Operation output", + "description": "", + "parent": "52060906-0000-4000-8000-000000000032", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionOutput": { + "outgoing_items": [ + "52060906-0000-4000-8000-000000000015" + ] + } + }, + "legacy": null + } + } + } + } + }, + "52060906-0000-4000-8000-000000000049": { + "id": "52060906-0000-4000-8000-000000000049", + "alias": "operation_probe_73", + "name": "Operation input", + "description": "", + "parent": "52060906-0000-4000-8000-000000000033", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "FunctionInput": { + "incoming_items": [ + "52060906-0000-4000-8000-000000000015" + ] + } + }, + "legacy": null + } + } + } + } + } + }, + "relations": { + "52060906-0000-4000-8000-00000000001e": { + "id": "52060906-0000-4000-8000-00000000001e", + "alias": "operation_probe_30", + "name": "InterfaceItemAllocation 30", + "description": "", + "body": { + "InterfaceItemAllocation": { + "source": "52060906-0000-4000-8000-000000000016", + "target": "52060906-0000-4000-8000-000000000014", + "order": 0, + "transmission": "Synchronous", + "acquisition": "Unspecified" + } + } + }, + "52060906-0000-4000-8000-00000000001f": { + "id": "52060906-0000-4000-8000-00000000001f", + "alias": "operation_probe_31", + "name": "InterfaceItemAllocation 31", + "description": "", + "body": { + "InterfaceItemAllocation": { + "source": "52060906-0000-4000-8000-000000000016", + "target": "52060906-0000-4000-8000-000000000015", + "order": 0, + "transmission": "Synchronous", + "acquisition": "Unspecified" + } + } + }, + "52060906-0000-4000-8000-000000000020": { + "id": "52060906-0000-4000-8000-000000000020", + "alias": "operation_probe_32", + "name": "InterfaceRequirement 32", + "description": "", + "body": { + "InterfaceRequirement": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-000000000016" + } + } + }, + "52060906-0000-4000-8000-000000000021": { + "id": "52060906-0000-4000-8000-000000000021", + "alias": "operation_probe_33", + "name": "InterfaceProvision 33", + "description": "", + "body": { + "InterfaceProvision": { + "source": "52060906-0000-4000-8000-00000000000d", + "target": "52060906-0000-4000-8000-000000000016" + } + } + }, + "52060906-0000-4000-8000-000000000022": { + "id": "52060906-0000-4000-8000-000000000022", + "alias": "operation_probe_34", + "name": "BoundaryExchange 34", + "description": "", + "body": { + "BoundaryExchange": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-00000000000d", + "oriented": false, + "items": [ + "52060906-0000-4000-8000-000000000014", + "52060906-0000-4000-8000-000000000015" + ] + } + } + }, + "52060906-0000-4000-8000-00000000003c": { + "id": "52060906-0000-4000-8000-00000000003c", + "alias": "operation_probe_60", + "name": "Allocation 60", + "description": "", + "body": { + "Allocation": { + "source": "52060906-0000-4000-8000-00000000000a", + "target": "52060906-0000-4000-8000-000000000032" + } + } + }, + "52060906-0000-4000-8000-00000000003d": { + "id": "52060906-0000-4000-8000-00000000003d", + "alias": "operation_probe_61", + "name": "Allocation 61", + "description": "", + "body": { + "Allocation": { + "source": "52060906-0000-4000-8000-00000000000b", + "target": "52060906-0000-4000-8000-000000000033" + } + } + }, + "52060906-0000-4000-8000-000000000050": { + "id": "52060906-0000-4000-8000-000000000050", + "alias": "operation_probe_80", + "name": "FunctionalExchange 80", + "description": "", + "body": { + "FunctionalExchange": { + "source": "52060906-0000-4000-8000-000000000032", + "target": "52060906-0000-4000-8000-000000000033", + "source_port": "52060906-0000-4000-8000-000000000046", + "target_port": "52060906-0000-4000-8000-000000000047", + "items": [ + "52060906-0000-4000-8000-000000000014" + ] + } + } + }, + "52060906-0000-4000-8000-000000000051": { + "id": "52060906-0000-4000-8000-000000000051", + "alias": "operation_probe_81", + "name": "FunctionalExchange 81", + "description": "", + "body": { + "FunctionalExchange": { + "source": "52060906-0000-4000-8000-000000000032", + "target": "52060906-0000-4000-8000-000000000033", + "source_port": "52060906-0000-4000-8000-000000000048", + "target_port": "52060906-0000-4000-8000-000000000049", + "items": [ + "52060906-0000-4000-8000-000000000015" + ] + } + } + }, + "52060906-0000-4000-8000-00000000005a": { + "id": "52060906-0000-4000-8000-00000000005a", + "alias": "operation_probe_90", + "name": "PortAllocation 90", + "description": "", + "body": { + "PortAllocation": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-000000000046" + } + } + }, + "52060906-0000-4000-8000-00000000005b": { + "id": "52060906-0000-4000-8000-00000000005b", + "alias": "operation_probe_91", + "name": "PortAllocation 91", + "description": "", + "body": { + "PortAllocation": { + "source": "52060906-0000-4000-8000-00000000000d", + "target": "52060906-0000-4000-8000-000000000047" + } + } + }, + "52060906-0000-4000-8000-00000000005c": { + "id": "52060906-0000-4000-8000-00000000005c", + "alias": "operation_probe_92", + "name": "PortAllocation 92", + "description": "", + "body": { + "PortAllocation": { + "source": "52060906-0000-4000-8000-00000000000c", + "target": "52060906-0000-4000-8000-000000000048" + } + } + }, + "52060906-0000-4000-8000-00000000005d": { + "id": "52060906-0000-4000-8000-00000000005d", + "alias": "operation_probe_93", + "name": "PortAllocation 93", + "description": "", + "body": { + "PortAllocation": { + "source": "52060906-0000-4000-8000-00000000000d", + "target": "52060906-0000-4000-8000-000000000049" + } + } + }, + "52060906-0000-4000-8000-000000000064": { + "id": "52060906-0000-4000-8000-000000000064", + "alias": "operation_probe_100", + "name": "ExchangeAllocation 100", + "description": "", + "body": { + "ExchangeAllocation": { + "source": "52060906-0000-4000-8000-000000000022", + "target": "52060906-0000-4000-8000-000000000050" + } + } + }, + "52060906-0000-4000-8000-000000000065": { + "id": "52060906-0000-4000-8000-000000000065", + "alias": "operation_probe_101", + "name": "ExchangeAllocation 101", + "description": "", + "body": { + "ExchangeAllocation": { + "source": "52060906-0000-4000-8000-000000000022", + "target": "52060906-0000-4000-8000-000000000051" + } + } + } + }, + "diagrams": {}, + "artifact_links": {}, + "traces": {} +} \ No newline at end of file diff --git a/harness/fixtures/interface-operation-probe/probe.rs b/harness/fixtures/interface-operation-probe/probe.rs new file mode 100644 index 00000000..813742c2 --- /dev/null +++ b/harness/fixtures/interface-operation-probe/probe.rs @@ -0,0 +1,335 @@ +//! Existing-core expressivity experiment, not a proposed production DTO. +use oa_core::validation::{Severity, validate}; +use oa_core::*; +use std::{collections::BTreeMap, fs, path::Path}; +fn id(n: u128) -> Id { + Id::from_u128(0x52060906000040008000000000000000 + n) +} +fn add(p: &mut Project, n: u128, k: Kind, name: &str, parent: Option) -> Id { + let mut e = if k == Kind::Port { + Element { + id: id(n), + alias: format!("operation_probe_{n}"), + name: name.into(), + description: String::new(), + parent, + body: ElementBody::shared_in( + Layer::Sa, + SharedBody::Port { + contract: PortContract::Component { + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }, + legacy: None, + }, + ), + } + } else { + p.make_element(k, Layer::Sa, name, parent).unwrap() + }; + e.id = id(n); + e.alias = format!("operation_probe_{n}"); + p.elements.insert(e.id, e); + id(n) +} +fn body(p: &mut Project, n: u128, b: SharedBody) { + *p.elements.get_mut(&id(n)).unwrap().shared_mut().unwrap() = b; +} +fn rel(p: &mut Project, n: u128, k: RelationKind, a: u128, b: u128) { + p.relations.insert( + id(n), + Relation { + id: id(n), + alias: format!("operation_probe_{n}"), + name: format!("{k:?} {n}"), + description: String::new(), + body: RelationBody::new(k, id(a), id(b)), + }, + ); +} +fn valid(p: &Project) { + let errors: Vec<_> = validate(p) + .into_iter() + .filter(|i| i.severity == Severity::Error) + .collect(); + assert!(errors.is_empty(), "{errors:?}"); +} +fn message(p: &Project, n: u128) -> &OccurrenceBody { + let Some(SharedBody::Occurrence { body, .. }) = p.elements[&id(n)].shared() else { + panic!() + }; + body +} +fn set_exchange(p: &mut Project, n: u128, target: Id) { + let Some(SharedBody::Occurrence { + body: OccurrenceBody::Exchange { exchange, .. }, + .. + }) = p.elements.get_mut(&id(n)).unwrap().shared_mut() + else { + panic!() + }; + *exchange = Some(target); +} +fn initial() -> Project { + let mut p = Project::empty(); + p.id = id(1); + p.name = "Two operations, one interface".into(); + add(&mut p, 10, Kind::SystemActor, "Operator console", None); + add(&mut p, 11, Kind::System, "Valve service", None); + for (port, owner) in [(12, 10), (13, 11)] { + add(&mut p, port, Kind::Port, "Service socket", Some(id(owner))); + body( + &mut p, + port, + SharedBody::Port { + contract: PortContract::Component { + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }, + legacy: None, + }, + ); + } + for (operation, name) in [(20, "Reset"), (21, "ReadStatus")] { + add(&mut p, operation, Kind::ExchangeItem, name, None); + body( + &mut p, + operation, + SharedBody::ExchangeItem { + kind: ExchangeItemKind::Information, + mechanism: ExchangeMechanism::Operation, + supertype: None, + }, + ); + } + add(&mut p, 22, Kind::Interface, "Valve commands", None); + for (allocation, item) in [(30, 20), (31, 21)] { + rel( + &mut p, + allocation, + RelationKind::InterfaceItemAllocation, + 22, + item, + ); + let RelationBody::InterfaceItemAllocation { transmission, .. } = + &mut p.relations.get_mut(&id(allocation)).unwrap().body + else { + panic!() + }; + *transmission = TransmissionProtocol::Synchronous; + } + rel(&mut p, 32, RelationKind::InterfaceRequirement, 12, 22); + rel(&mut p, 33, RelationKind::InterfaceProvision, 13, 22); + rel(&mut p, 34, RelationKind::BoundaryExchange, 12, 13); + p.relations.get_mut(&id(34)).unwrap().body = RelationBody::BoundaryExchange { + source: id(12), + target: id(13), + oriented: false, + items: vec![id(20), id(21)], + }; + add(&mut p, 40, Kind::Scenario, "Reset then read status", None); + body( + &mut p, + 40, + SharedBody::Scenario { + viewpoint: ScenarioViewpoint::BoundaryInterface, + context: None, + precondition: String::new(), + postcondition: String::new(), + steps: vec![id(43), id(44)], + }, + ); + for (line, participant) in [(41, 10), (42, 11)] { + add( + &mut p, + line, + Kind::Lifeline, + "Service participant", + Some(id(40)), + ); + body( + &mut p, + line, + SharedBody::Lifeline { + represented: Some(id(participant)), + }, + ); + } + for (call, name) in [(43, "Call Reset"), (44, "Call ReadStatus")] { + add(&mut p, call, Kind::Occurrence, name, Some(id(40))); + body( + &mut p, + call, + SharedBody::Occurrence { + body: OccurrenceBody::Exchange { + exchange: Some(id(34)), + sender: Some(id(41)), + receiver: Some(id(42)), + message_kind: MessageKind::Call, + reply_to: None, + values: vec![], + }, + precondition: String::new(), + postcondition: String::new(), + }, + ); + } + valid(&p); + p +} +fn refined(p: &Project) -> Project { + let mut q = p.clone(); + for (work, owner, name) in [ + (50, 10, "Issue service request"), + (51, 11, "Execute service request"), + ] { + add(&mut q, work, Kind::Function, name, None); + rel(&mut q, work + 10, RelationKind::Allocation, owner, work); + } + for (item, output, input, exchange) in [(20, 70, 71, 80), (21, 72, 73, 81)] { + add(&mut q, output, Kind::Port, "Operation output", Some(id(50))); + add(&mut q, input, Kind::Port, "Operation input", Some(id(51))); + body( + &mut q, + output, + SharedBody::Port { + contract: PortContract::FunctionOutput { + outgoing_items: vec![id(item)], + }, + legacy: None, + }, + ); + body( + &mut q, + input, + SharedBody::Port { + contract: PortContract::FunctionInput { + incoming_items: vec![id(item)], + }, + legacy: None, + }, + ); + rel(&mut q, exchange, RelationKind::FunctionalExchange, 50, 51); + q.relations.get_mut(&id(exchange)).unwrap().body = RelationBody::FunctionalExchange { + source: id(50), + target: id(51), + source_port: id(output), + target_port: id(input), + items: vec![id(item)], + }; + rel( + &mut q, + output + 20, + RelationKind::PortAllocation, + 12, + output, + ); + rel(&mut q, input + 20, RelationKind::PortAllocation, 13, input); + rel( + &mut q, + exchange + 20, + RelationKind::ExchangeAllocation, + 34, + exchange, + ); + } + set_exchange(&mut q, 43, id(80)); + set_exchange(&mut q, 44, id(81)); + valid(&q); + q +} +fn write(p: &Project, out: &Path, name: &str) { + let source = language::format(p); + let bundle = source_project::SourceBundle { + entry: "entry.arcadia".into(), + files: BTreeMap::from([("entry.arcadia".into(), source.clone())]), + }; + assert_eq!( + &source_project::compile_bundle(&bundle, Default::default()) + .unwrap() + .decoded + .project, + p + ); + assert_eq!( + &serde_json::from_value::(serde_json::to_value(p).unwrap()).unwrap(), + p + ); + fs::write(out.join(format!("{name}.arcadia")), source).unwrap(); + fs::write( + out.join(format!("{name}.json")), + serde_json::to_vec_pretty(p).unwrap(), + ) + .unwrap(); +} +fn main() { + let directory = std::env::args().nth(1).expect("probe OUTPUT_DIRECTORY"); + let out = Path::new(&directory); + fs::create_dir_all(out).unwrap(); + let p = initial(); + write(&p, out, "boundary-only"); + assert_eq!( + message(&p, 43), + message(&p, 44), + "Only prose distinguishes the intended operations" + ); + assert!(!references::element_references(&p.elements[&id(43)]).contains(&id(20))); + assert!(!references::element_references(&p.elements[&id(44)]).contains(&id(21))); + for target in [20, 30] { + let mut invalid = p.clone(); + set_exchange(&mut invalid, 43, id(target)); + assert!( + validate(&invalid) + .iter() + .any(|i| i.code == "occurrence_exchange" && i.element == Some(id(43))) + ); + } + let mut proposed = serde_json::to_value(message(&p, 43)).unwrap(); + proposed["Exchange"]["operation"] = serde_json::to_value(id(30)).unwrap(); + assert!( + serde_json::from_value::(proposed) + .unwrap_err() + .to_string() + .contains("unknown field") + ); + let mut session = Session::new(p.clone()); + let mut reset = p.elements[&id(20)].clone(); + reset.name = "EmergencyReset".into(); + session + .execute(Command::EditElement(id(20), reset)) + .unwrap(); + assert_eq!(message(session.project(), 43), message(&p, 43)); + session.execute(Command::Delete(id(20))).unwrap(); + assert_eq!(message(session.project(), 43), message(&p, 43)); + assert!(session.project().elements.contains_key(&id(43))); + valid(session.project()); + write(session.project(), out, "after-reset-deletion"); + let q = refined(&p); + write(&q, out, "explicit-functional-refinement"); + assert_eq!(q.relations[&id(34)], p.relations[&id(34)]); + assert_ne!(message(&q, 43), message(&q, 44)); + for n in [43, 44] { + assert!( + references::element_references(&q.elements[&id(n)]) + .iter() + .any(|v| [id(80), id(81)].contains(v)) + ); + } + println!( + "PASS validated boundary-only model: {} elements / {} relations, two parameterless Operations", + p.elements.len(), + p.relations.len() + ); + println!( + "PASS identical typed Calls; item/allocation exchange substitutions rejected; operation DTO field rejected" + ); + println!( + "PASS operation rename/delete leaves intended Call's typed body unchanged; Call remains structurally valid" + ); + println!( + "PASS distinct FE Calls require this explicit refinement: +{} elements / +{} relations; same original boundary", + q.elements.len() - p.elements.len(), + q.relations.len() - p.relations.len() + ); + println!("PASS all three complete source/JSON round trips; no production mutation"); +} diff --git a/harness/fixtures/release1-native/README.md b/harness/fixtures/release1-native/README.md index 621f9676..337c442a 100644 --- a/harness/fixtures/release1-native/README.md +++ b/harness/fixtures/release1-native/README.md @@ -1,9 +1,11 @@ -# First-release native acceptance fixtures - -Synthetic, identity-stable source/JSON models and direct-core expected outputs for the current source-v5 continuation. Work on copies. The first user-facing state and keyed review trials were performed with Windows binary SHA25670069D6CBBE1917E1597794B3E190FF9CF23664D458F794B3270DDBE6EE683A2; see ../../release-current-native-verification.md for exact accepted scope and open findings. Ordered fixtures are validated oracles awaiting native acceptance, not evidence that those workflows have passed. - -- state-actions.arcadia: typed Entry/Do/Exit rows, legacy notes, Internal transition and nested region. Initial14elements/10relations/one8-placement state view. With the new advisory coverage pass, uncovered function warnings are expected; they do not invalidate the model. -- semantic-keyed.steps.md: Annotation Take and rejected/repaired Binding Candidate, with exact source and JSON oracles, frozen requests, plans and identities. The independent expected sources preserve the leading comment and row order. -- semantic-ordered.steps.md: ordered member insertion ambiguity and explicit final order. Main, reverse, Scenario, Branch steps and Fragment branch variants each include initial/final source and JSON, request, plan and manifest. - -The steps originally name .local/native-fixtures, the isolated trial directory; use a copied directory here equivalently. Never overwrite an oracle with a native Save. Versioned source snapshots must be migrated deliberately when the normative source version changes; keep historical native evidence scoped to its actual binary. These fixtures do not replace the Rust integration tests or constitute complete release acceptance. +# First-release native acceptance fixtures + +Source-6 additions: [`artifacts/`](artifacts/README.md) preserves artifact move/deletion/creation requests, exact oracles, manifests, native instructions and a configurable-output generator. Its automated checks passed; artifact native acceptance remains pending. `semantic-batch-native.md` specifies the two-source range review and exact initial/accepted source and JSON oracles. `dense-40/` contains the deterministic forty-exchange view and eleven visibility matrices generated by `cargo run --locked -p oa-core --example release_dense_views -- OUTPUT_DIRECTORY`. These additions have passed core generation and semantic validation; native acceptance remains separately recorded. Keep the earlier versioned fixtures below unchanged so their historical evidence remains reproducible. + +Historical source-v5 fixtures below are synthetic, identity-stable source/JSON models and direct-core expected outputs. Work on copies. The first user-facing state and keyed review trials were performed with Windows binary SHA25670069D6CBBE1917E1597794B3E190FF9CF23664D458F794B3270DDBE6EE683A2; see ../../release-current-native-verification.md for exact accepted scope and open findings. The current native record now includes scoped main and reverse ordered Apply/Undo/Redo evidence; remaining ordered variants retain their separate acceptance gates. + +- state-actions.arcadia: typed Entry/Do/Exit rows, legacy notes, Internal transition and nested region. Initial14elements/10relations/one8-placement state view. With the new advisory coverage pass, uncovered function warnings are expected; they do not invalidate the model. +- semantic-keyed.steps.md: Annotation Take and rejected/repaired Binding Candidate, with exact source and JSON oracles, frozen requests, plans and identities. The independent expected sources preserve the leading comment and row order. +- semantic-ordered.steps.md: ordered member insertion ambiguity and explicit final order. Main, reverse, Scenario, Branch steps and Fragment branch variants each include initial/final source and JSON, request, plan and manifest. + +The steps originally name .local/native-fixtures, the isolated trial directory; use a copied directory here equivalently. Never overwrite an oracle with a native Save. Versioned source snapshots must be migrated deliberately when the normative source version changes; keep historical native evidence scoped to its actual binary. These fixtures do not replace the Rust integration tests or constitute complete release acceptance. diff --git a/harness/fixtures/release1-native/artifacts/README.md b/harness/fixtures/release1-native/artifacts/README.md new file mode 100644 index 00000000..c10b78d9 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/README.md @@ -0,0 +1,38 @@ +# Artifact lifecycle native fixtures + +These source-6 fixtures preserve direct-core expected results for the implemented artifact review controls. **MOVE, CREATE and DELETE passed native exact-source Save/Undo/Redo checks: nine saves succeeded first attempt.** MOVE invalid raw/optional state and CREATE excluded row/reserved identity also survived controlled-stop recovery exactly. Full desktop 518/518 and the corrected six-artifact filter passed at the implementation checkpoint; strict all-target workspace Clippy passed separately. See [the native lifecycle evidence](../../../release-artifacts-native-20260906.md), [implementation evidence](../../../release-semantic-artifacts.md), and [native steps](semantic-artifacts-native.md). These native runs used the preceding binary; subsequent diagnostic/reserved-identity wording changes require their own verification. Destination-only retention and every possible owner/type were not separately exercised natively. + +Work on copies. Each `semantic-artifacts-{move,delete,create}` set contains original and expected `.arcadia`/`.json` files, a complete core request, and an identity/count manifest. `semantic-artifacts.hashes.json` lists portable relative filenames and SHA256 hashes. Every fixture uses **Interlock requirement rationale** as its owner trace. + +- Move: explicitly review Owner, Revision `Some("")`, and Fragment `None`; two links remain. +- Delete: the source link remains present under another owner; Take Presence removes the destination link. Source metadata remains, so this fixture has eight acknowledged facts and no retired metadata. +- Create: an existing source link seeds a reserved destination counterpart. Presence plus all seven metadata facets must be explicitly reviewed; one link becomes two. + +The generator validates every request with `plan_semantic_transition` and `prepare_semantic_transition`, updates the exact source bundle through `source_project::update_bundle`, and reparses both source oracles. No GUI action is simulated. + +## Generator usage + +`generate-semantic-artifacts OUTPUT_DIRECTORY [RESERVED_CREATION_UUID]` + +An explicit output directory is required. Without a UUID it generates fresh initial fixture identities; use a new scratch directory. With a UUID it loads the existing creation fixture JSON and exact original source in that directory, preserves both base files, and writes the creation after oracle, request and manifest for the UI's reserved identity. Do not regenerate initial fixtures after starting native work. + +The checked Windows compilation recipe uses the already-built workspace libraries and the user-local GNU LLVM toolchain; it writes its executable only under ignored `.local`. Library filenames belong to that build and must be updated together if a different build changes their fingerprints. No executable is committed. + +```powershell +. .local/toolchain/env.ps1 +rustc --edition 2024 -C linker=clang.exe --extern oa_core=.local/target-gnullvm/debug/deps/liboa_core-6c8da57367eec642.rlib --extern serde_json=.local/target-gnullvm/debug/deps/libserde_json-da2c4f333fc47818.rlib -L dependency=.local/target-gnullvm/debug/deps harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs -o .local/native-fixtures/generate-semantic-artifacts-preserved.exe +``` + +Copy this directory into an isolated trial directory, for example `.local/native-fixtures/artifacts-session`. After the GUI reserves its UUID, regenerate the exact creation oracle: + +```powershell +.local/native-fixtures/generate-semantic-artifacts-preserved.exe .local/native-fixtures/artifacts-session +``` + +For fresh fixtures with new initial identities, omit the UUID and select a new directory: + +```powershell +.local/native-fixtures/generate-semantic-artifacts-preserved.exe .local/native-fixtures/artifacts-fresh +``` + +The native steps explain why the static creation after file is a template until the UI's reserved UUID is supplied. Keep model/source/Undo/Redo comparisons exact after that substitution. Refresh local hashes after regenerating a creation oracle; the committed hash inventory continues to describe the committed template. diff --git a/harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs b/harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs new file mode 100644 index 00000000..17ea180e --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/generate-semantic-artifacts.rs @@ -0,0 +1,263 @@ +use oa_core::{transitions::*, *}; +use std::{collections::BTreeMap, fs, path::Path}; +const META: [ArtifactFacet; 7] = [ + ArtifactFacet::Name, + ArtifactFacet::Description, + ArtifactFacet::Owner, + ArtifactFacet::Relation, + ArtifactFacet::Uri, + ArtifactFacet::Revision, + ArtifactFacet::Fragment, +]; +fn request( + p: &Project, + trace: &Trace, + keys: KeyPair, + decisions: Vec<(ArtifactFacet, SemanticDecision)>, + candidate: Option, +) -> SemanticTransitionRequest { + let mut t = TransitionRequest::new(TransitionDirection::OaToSa, vec![trace.oa]); + t.mappings.push(TransitionMapping { + source: trace.oa, + target: Some(trace.sa), + target_kind: None, + }); + let mut r = SemanticTransitionRequest::new(t); + r.traces.insert(trace.oa, trace.id); + r.references.insert(trace.oa, trace.sa); + if let Some((oa, sa)) = keys.oa.zip(keys.sa) { + r.references.insert(oa, sa); + } + r.reviews.push(SemanticReviewRequest { + source: trace.oa, + resolutions: decisions + .into_iter() + .map(|(facet, decision)| FacetResolution { + facet: FacetSelector::Artifact { + keys: keys.clone(), + facet, + }, + decision, + }) + .collect(), + reset: vec![], + }); + if let Some(c) = candidate { + r.candidates.push(SemanticRecord::Artifact(c)); + } + validate_candidate(p).unwrap(); + r +} +fn emit(dir: &Path, p: &Project, r: SemanticTransitionRequest, name: &str, preserve_base: bool) { + let plan = plan_semantic_transition(p, r.clone()).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let after = prepare_semantic_transition(p, &plan).unwrap().project; + let logical = format!("{name}.arcadia"); + let source = if preserve_base { + fs::read_to_string(dir.join(&logical)).expect("Read unchanged fixture source") + } else { + format!( + "// Exact artifact rationale: no implicit metadata review\r\n{}", + language::format(p) + ) + }; + let bundle = source_project::SourceBundle { + entry: logical.clone(), + files: BTreeMap::from([(logical.clone(), source.clone())]), + }; + let compiled = + source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap(); + let updated = + source_project::update_bundle(&compiled, &after, source_project::ProjectLimits::default()) + .unwrap(); + for (suffix, project, text) in [ + ("", p, &source), + (".after", &after, &updated.bundle.files[&logical]), + ] { + assert_eq!(language::parse(text).unwrap(), *project); + if preserve_base && suffix.is_empty() { + continue; + } + fs::write(dir.join(format!("{name}{suffix}.arcadia")), text).unwrap(); + fs::write( + dir.join(format!("{name}{suffix}.json")), + serde_json::to_vec_pretty(project).unwrap(), + ) + .unwrap(); + } + fs::write( + dir.join(format!("{name}.request.json")), + serde_json::to_vec_pretty(&r).unwrap(), + ) + .unwrap(); + let manifest = serde_json::json!({ + "fixture":name,"schema":p.schema_version,"project":p.id, + "trace_ids":r.traces.values().collect::>(), + "candidate_ids":r.candidates.iter().map(SemanticRecord::id).collect::>(), + "initial_artifact_ids":p.artifact_links.keys().collect::>(), + "final_artifact_ids":after.artifact_links.keys().collect::>(), + "acknowledged_count":plan.reviews.iter().map(|review|review.acknowledged.len()).sum::(), + "retired_count":plan.reviews.iter().map(|review|review.retired.len()).sum::(), + "impact_count":plan.impacts.len(),"native_acceptance":"pending" + }); + fs::write( + dir.join(format!("{name}.manifest.json")), + serde_json::to_vec_pretty(&manifest).unwrap(), + ) + .unwrap(); + println!( + "{name}: {} artifacts -> {}; {} reviewed facts, {} retired, {} impacts", + p.artifact_links.len(), + after.artifact_links.len(), + plan.reviews[0].acknowledged.len(), + plan.reviews[0].retired.len(), + plan.impacts.len() + ); +} +fn main() { + let args: Vec<_> = std::env::args().collect(); + assert!( + (2..=3).contains(&args.len()), + "Usage: generate-semantic-artifacts OUTPUT_DIRECTORY [RESERVED_CREATION_UUID]. Without UUID, creates fresh fixture identities; with UUID, preserves the existing creation base and writes its exact after oracle." + ); + let dir = Path::new(&args[1]); + fs::create_dir_all(dir).unwrap(); + if args.len() == 3 { + let p: Project = + serde_json::from_slice(&fs::read(dir.join("semantic-artifacts-create.json")).unwrap()) + .unwrap(); + let trace = p + .traces + .values() + .find(|t| t.alias == "artifact_review") + .unwrap(); + let source = p + .artifact_links + .values() + .find(|a| a.owner == trace.oa) + .unwrap(); + let mut candidate = source.clone(); + candidate.id = args[2].parse().unwrap(); + candidate.alias = format!("artifact_{}", candidate.id.simple()); + candidate.owner = trace.sa; + let keys = KeyPair { + oa: Some(source.id), + sa: Some(candidate.id), + }; + let mut decisions = vec![(ArtifactFacet::Presence, SemanticDecision::TakeSource)]; + decisions.extend(META.map(|facet| (facet, SemanticDecision::Candidate))); + emit( + dir, + &p, + request(&p, trace, keys, decisions, Some(candidate)), + "semantic-artifacts-create", + true, + ); + return; + } + let mut p = Project::empty(); + p.name = "Explicit requirement-link review".into(); + let mut owners = vec![]; + let mut others = vec![]; + let mut links = vec![]; + for layer in [Layer::Oa, Layer::Sa] { + let prefix = if layer == Layer::Oa { "OA" } else { "SA" }; + let kind = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + let owner = p + .add_element(kind, layer, &format!("{prefix} Startup coordination"), None) + .unwrap(); + let other = p + .add_element(kind, layer, &format!("{prefix} Safety coordination"), None) + .unwrap(); + let id = Id::new_v4(); + p.artifact_links.insert( + id, + ArtifactLink { + id, + alias: format!("artifact_{}", id.simple()), + name: format!("{prefix} Interlock requirement"), + description: "Reviewed safety rationale\r\nSecond exact line".into(), + owner, + relation: ArtifactRelation::Satisfies, + uri: "requirements/interlock".into(), + revision: None, + fragment: Some("section-4".into()), + }, + ); + owners.push(owner); + others.push(other); + links.push(id); + } + let id = Id::new_v4(); + let trace = Trace { + id, + alias: "artifact_review".into(), + name: "Interlock requirement rationale".into(), + description: String::new(), + oa: owners[0], + sa: owners[1], + kind: TraceKind::Manual, + baseline: None, + }; + p.traces.insert(id, trace.clone()); + let keys = KeyPair { + oa: Some(links[0]), + sa: Some(links[1]), + }; + let mut moved = p.artifact_links[&links[1]].clone(); + moved.owner = others[1]; + moved.revision = Some(String::new()); + moved.fragment = None; + emit( + dir, + &p, + request( + &p, + &trace, + keys.clone(), + vec![ + (ArtifactFacet::Owner, SemanticDecision::Candidate), + (ArtifactFacet::Revision, SemanticDecision::Candidate), + (ArtifactFacet::Fragment, SemanticDecision::Candidate), + ], + Some(moved), + ), + "semantic-artifacts-move", + false, + ); + let mut deleted = p.clone(); + deleted.artifact_links.get_mut(&links[0]).unwrap().owner = others[0]; + emit( + dir, + &deleted, + request( + &deleted, + &trace, + keys.clone(), + vec![(ArtifactFacet::Presence, SemanticDecision::TakeSource)], + None, + ), + "semantic-artifacts-delete", + false, + ); + let mut create = p.clone(); + create.artifact_links.remove(&links[1]); + let source = create.artifact_links[&links[0]].clone(); + let mut candidate = source; + candidate.id = links[1]; + candidate.alias = format!("artifact_{}", candidate.id.simple()); + candidate.owner = owners[1]; + let mut decisions = vec![(ArtifactFacet::Presence, SemanticDecision::TakeSource)]; + decisions.extend(META.map(|facet| (facet, SemanticDecision::Candidate))); + emit( + dir, + &create, + request(&create, &trace, keys, decisions, Some(candidate)), + "semantic-artifacts-create", + false, + ); +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia new file mode 100644 index 00000000..dc559cef --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.arcadia @@ -0,0 +1,202 @@ +// Exact artifact rationale: no implicit metadata review +arcadia 6; +project mission "Explicit requirement-link review" id "a1db88f7-0af1-4ce9-825e-2a407eaa647a" { + description ""; + + oa_activity oa_safety_coordination "OA Safety coordination" id "05617eb5-8a1b-4c79-a957-d89567939995" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "9b07222e-132a-4505-a40e-5761277e43b4" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "bd6e974f-1f72-4ed9-b146-72b272f4c861" { + description ""; + parent none; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" { + description ""; + parent none; + } + + artifact artifact_f3635689622e4e71849aec332f8931f3 "OA Interlock requirement" id "f3635689-622e-4e71-849a-ec332f8931f3" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @oa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + trace artifact_review "Interlock requirement rationale" id "f27b20ea-1a66-4a91-87d6-79b7bda09a13" { + baseline { + oa { + description ""; + name "OA Startup coordination"; + }; + sa { + description ""; + name "SA Startup coordination"; + }; + semantic { + facets [{ + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Presence { + oa { + Value true; + }; + sa { + Value true; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Name { + oa { + Value "OA Interlock requirement"; + }; + sa { + Value "OA Interlock requirement"; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Description { + oa { + Value "Reviewed safety rationale\r\nSecond exact line"; + }; + sa { + Value "Reviewed safety rationale\r\nSecond exact line"; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Owner { + oa { + Value uuid "d4d778ce-95c7-4e83-82ba-d129c6b61dc9"; + }; + sa { + Value uuid "9b07222e-132a-4505-a40e-5761277e43b4"; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Relation { + oa { + Value Satisfies; + }; + sa { + Value Satisfies; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Uri { + oa { + Value "requirements/interlock"; + }; + sa { + Value "requirements/interlock"; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Revision { + oa { + Value none; + }; + sa { + Value none; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Fragment { + oa { + Value "section-4"; + }; + sa { + Value "section-4"; + }; + }; + }; + }; + }]; + }; + }; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + + + artifact artifact_9882d6d1e230418da7514924db03fcc1 "OA Interlock requirement" id "9882d6d1-e230-418d-a751-4924db03fcc1" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @sa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json new file mode 100644 index 00000000..650e800f --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.after.json @@ -0,0 +1,244 @@ +{ + "schema_version": 6, + "id": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "alias": "mission", + "name": "Explicit requirement-link review", + "description": "", + "elements": { + "05617eb5-8a1b-4c79-a957-d89567939995": { + "id": "05617eb5-8a1b-4c79-a957-d89567939995", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "9b07222e-132a-4505-a40e-5761277e43b4": { + "id": "9b07222e-132a-4505-a40e-5761277e43b4", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "bd6e974f-1f72-4ed9-b146-72b272f4c861": { + "id": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": { + "id": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": { + "9882d6d1-e230-418d-a751-4924db03fcc1": { + "id": "9882d6d1-e230-418d-a751-4924db03fcc1", + "alias": "artifact_9882d6d1e230418da7514924db03fcc1", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "9b07222e-132a-4505-a40e-5761277e43b4", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + }, + "f3635689-622e-4e71-849a-ec332f8931f3": { + "id": "f3635689-622e-4e71-849a-ec332f8931f3", + "alias": "artifact_f3635689622e4e71849aec332f8931f3", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + }, + "traces": { + "f27b20ea-1a66-4a91-87d6-79b7bda09a13": { + "id": "f27b20ea-1a66-4a91-87d6-79b7bda09a13", + "alias": "artifact_review", + "name": "Interlock requirement rationale", + "description": "", + "oa": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "sa": "9b07222e-132a-4505-a40e-5761277e43b4", + "kind": "Manual", + "baseline": { + "oa": { + "name": "OA Startup coordination", + "description": "" + }, + "sa": { + "name": "SA Startup coordination", + "description": "" + }, + "semantic": { + "facets": [ + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Presence": { + "oa": { + "Value": true + }, + "sa": { + "Value": true + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Name": { + "oa": { + "Value": "OA Interlock requirement" + }, + "sa": { + "Value": "OA Interlock requirement" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Description": { + "oa": { + "Value": "Reviewed safety rationale\r\nSecond exact line" + }, + "sa": { + "Value": "Reviewed safety rationale\r\nSecond exact line" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Owner": { + "oa": { + "Value": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" + }, + "sa": { + "Value": "9b07222e-132a-4505-a40e-5761277e43b4" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Relation": { + "oa": { + "Value": "Satisfies" + }, + "sa": { + "Value": "Satisfies" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Uri": { + "oa": { + "Value": "requirements/interlock" + }, + "sa": { + "Value": "requirements/interlock" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Revision": { + "oa": { + "Value": null + }, + "sa": { + "Value": null + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Fragment": { + "oa": { + "Value": "section-4" + }, + "sa": { + "Value": "section-4" + } + } + } + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia new file mode 100644 index 00000000..40db7dd7 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia @@ -0,0 +1,43 @@ +// Exact artifact rationale: no implicit metadata review +arcadia 6; +project mission "Explicit requirement-link review" id "a1db88f7-0af1-4ce9-825e-2a407eaa647a" { + description ""; + + oa_activity oa_safety_coordination "OA Safety coordination" id "05617eb5-8a1b-4c79-a957-d89567939995" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "9b07222e-132a-4505-a40e-5761277e43b4" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "bd6e974f-1f72-4ed9-b146-72b272f4c861" { + description ""; + parent none; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" { + description ""; + parent none; + } + + artifact artifact_f3635689622e4e71849aec332f8931f3 "OA Interlock requirement" id "f3635689-622e-4e71-849a-ec332f8931f3" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @oa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + trace artifact_review "Interlock requirement rationale" id "f27b20ea-1a66-4a91-87d6-79b7bda09a13" { + baseline none; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json new file mode 100644 index 00000000..c82aab3d --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.json @@ -0,0 +1,76 @@ +{ + "schema_version": 6, + "id": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "alias": "mission", + "name": "Explicit requirement-link review", + "description": "", + "elements": { + "05617eb5-8a1b-4c79-a957-d89567939995": { + "id": "05617eb5-8a1b-4c79-a957-d89567939995", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "9b07222e-132a-4505-a40e-5761277e43b4": { + "id": "9b07222e-132a-4505-a40e-5761277e43b4", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "bd6e974f-1f72-4ed9-b146-72b272f4c861": { + "id": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": { + "id": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": { + "f3635689-622e-4e71-849a-ec332f8931f3": { + "id": "f3635689-622e-4e71-849a-ec332f8931f3", + "alias": "artifact_f3635689622e4e71849aec332f8931f3", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + }, + "traces": { + "f27b20ea-1a66-4a91-87d6-79b7bda09a13": { + "id": "f27b20ea-1a66-4a91-87d6-79b7bda09a13", + "alias": "artifact_review", + "name": "Interlock requirement rationale", + "description": "", + "oa": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "sa": "9b07222e-132a-4505-a40e-5761277e43b4", + "kind": "Manual", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json new file mode 100644 index 00000000..2b18bc5f --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.manifest.json @@ -0,0 +1,22 @@ +{ + "fixture": "semantic-artifacts-create", + "schema": 6, + "project": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "trace_ids": [ + "f27b20ea-1a66-4a91-87d6-79b7bda09a13" + ], + "candidate_ids": [ + "9882d6d1-e230-418d-a751-4924db03fcc1" + ], + "initial_artifact_ids": [ + "f3635689-622e-4e71-849a-ec332f8931f3" + ], + "final_artifact_ids": [ + "9882d6d1-e230-418d-a751-4924db03fcc1", + "f3635689-622e-4e71-849a-ec332f8931f3" + ], + "acknowledged_count": 8, + "retired_count": 0, + "impact_count": 0, + "native_acceptance": "pending" +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json new file mode 100644 index 00000000..d415f668 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-create.request.json @@ -0,0 +1,147 @@ +{ + "transition": { + "direction": "OaToSa", + "sources": [ + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "target": "9b07222e-132a-4505-a40e-5761277e43b4", + "target_kind": null + } + ], + "fields": [], + "resolutions": [], + "destination_diagram": null, + "communication_endpoints": [] + }, + "traces": { + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": "f27b20ea-1a66-4a91-87d6-79b7bda09a13" + }, + "references": { + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": "9b07222e-132a-4505-a40e-5761277e43b4", + "f3635689-622e-4e71-849a-ec332f8931f3": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "reviews": [ + { + "source": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "resolutions": [ + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Presence" + } + }, + "decision": "TakeSource" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Name" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Description" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Owner" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Relation" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Uri" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Revision" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Fragment" + } + }, + "decision": "Candidate" + } + ], + "reset": [] + } + ], + "candidates": [ + { + "Artifact": { + "id": "9882d6d1-e230-418d-a751-4924db03fcc1", + "alias": "artifact_9882d6d1e230418da7514924db03fcc1", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "9b07222e-132a-4505-a40e-5761277e43b4", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + } + ], + "accepted_impacts": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia new file mode 100644 index 00000000..b178c6cf --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.arcadia @@ -0,0 +1,190 @@ +// Exact artifact rationale: no implicit metadata review +arcadia 6; +project mission "Explicit requirement-link review" id "a1db88f7-0af1-4ce9-825e-2a407eaa647a" { + description ""; + + oa_activity oa_safety_coordination "OA Safety coordination" id "05617eb5-8a1b-4c79-a957-d89567939995" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "9b07222e-132a-4505-a40e-5761277e43b4" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "bd6e974f-1f72-4ed9-b146-72b272f4c861" { + description ""; + parent none; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" { + description ""; + parent none; + } + + + + + + + + + + + + + + + artifact artifact_f3635689622e4e71849aec332f8931f3 "OA Interlock requirement" id "f3635689-622e-4e71-849a-ec332f8931f3" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @oa_safety_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + trace artifact_review "Interlock requirement rationale" id "f27b20ea-1a66-4a91-87d6-79b7bda09a13" { + baseline { + oa { + description ""; + name "OA Startup coordination"; + }; + sa { + description ""; + name "SA Startup coordination"; + }; + semantic { + facets [{ + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Presence { + oa { + Value false; + }; + sa { + Value false; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Name { + oa { + Value "OA Interlock requirement"; + }; + sa NotApplicable; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Description { + oa { + Value "Reviewed safety rationale\r\nSecond exact line"; + }; + sa NotApplicable; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Owner { + oa { + Value uuid "05617eb5-8a1b-4c79-a957-d89567939995"; + }; + sa NotApplicable; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Relation { + oa { + Value Satisfies; + }; + sa NotApplicable; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Uri { + oa { + Value "requirements/interlock"; + }; + sa NotApplicable; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Revision { + oa { + Value none; + }; + sa NotApplicable; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Fragment { + oa { + Value "section-4"; + }; + sa NotApplicable; + }; + }; + }; + }]; + }; + }; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json new file mode 100644 index 00000000..dbb6e151 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.after.json @@ -0,0 +1,219 @@ +{ + "schema_version": 6, + "id": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "alias": "mission", + "name": "Explicit requirement-link review", + "description": "", + "elements": { + "05617eb5-8a1b-4c79-a957-d89567939995": { + "id": "05617eb5-8a1b-4c79-a957-d89567939995", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "9b07222e-132a-4505-a40e-5761277e43b4": { + "id": "9b07222e-132a-4505-a40e-5761277e43b4", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "bd6e974f-1f72-4ed9-b146-72b272f4c861": { + "id": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": { + "id": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": { + "f3635689-622e-4e71-849a-ec332f8931f3": { + "id": "f3635689-622e-4e71-849a-ec332f8931f3", + "alias": "artifact_f3635689622e4e71849aec332f8931f3", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "05617eb5-8a1b-4c79-a957-d89567939995", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + }, + "traces": { + "f27b20ea-1a66-4a91-87d6-79b7bda09a13": { + "id": "f27b20ea-1a66-4a91-87d6-79b7bda09a13", + "alias": "artifact_review", + "name": "Interlock requirement rationale", + "description": "", + "oa": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "sa": "9b07222e-132a-4505-a40e-5761277e43b4", + "kind": "Manual", + "baseline": { + "oa": { + "name": "OA Startup coordination", + "description": "" + }, + "sa": { + "name": "SA Startup coordination", + "description": "" + }, + "semantic": { + "facets": [ + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Presence": { + "oa": { + "Value": false + }, + "sa": { + "Value": false + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Name": { + "oa": { + "Value": "OA Interlock requirement" + }, + "sa": "NotApplicable" + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Description": { + "oa": { + "Value": "Reviewed safety rationale\r\nSecond exact line" + }, + "sa": "NotApplicable" + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Owner": { + "oa": { + "Value": "05617eb5-8a1b-4c79-a957-d89567939995" + }, + "sa": "NotApplicable" + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Relation": { + "oa": { + "Value": "Satisfies" + }, + "sa": "NotApplicable" + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Uri": { + "oa": { + "Value": "requirements/interlock" + }, + "sa": "NotApplicable" + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Revision": { + "oa": { + "Value": null + }, + "sa": "NotApplicable" + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Fragment": { + "oa": { + "Value": "section-4" + }, + "sa": "NotApplicable" + } + } + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia new file mode 100644 index 00000000..32bb6960 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.arcadia @@ -0,0 +1,52 @@ +// Exact artifact rationale: no implicit metadata review +arcadia 6; +project mission "Explicit requirement-link review" id "a1db88f7-0af1-4ce9-825e-2a407eaa647a" { + description ""; + + oa_activity oa_safety_coordination "OA Safety coordination" id "05617eb5-8a1b-4c79-a957-d89567939995" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "9b07222e-132a-4505-a40e-5761277e43b4" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "bd6e974f-1f72-4ed9-b146-72b272f4c861" { + description ""; + parent none; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" { + description ""; + parent none; + } + + artifact artifact_9882d6d1e230418da7514924db03fcc1 "SA Interlock requirement" id "9882d6d1-e230-418d-a751-4924db03fcc1" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @sa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + artifact artifact_f3635689622e4e71849aec332f8931f3 "OA Interlock requirement" id "f3635689-622e-4e71-849a-ec332f8931f3" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @oa_safety_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + trace artifact_review "Interlock requirement rationale" id "f27b20ea-1a66-4a91-87d6-79b7bda09a13" { + baseline none; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json new file mode 100644 index 00000000..f63c5c21 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.json @@ -0,0 +1,87 @@ +{ + "schema_version": 6, + "id": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "alias": "mission", + "name": "Explicit requirement-link review", + "description": "", + "elements": { + "05617eb5-8a1b-4c79-a957-d89567939995": { + "id": "05617eb5-8a1b-4c79-a957-d89567939995", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "9b07222e-132a-4505-a40e-5761277e43b4": { + "id": "9b07222e-132a-4505-a40e-5761277e43b4", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "bd6e974f-1f72-4ed9-b146-72b272f4c861": { + "id": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": { + "id": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": { + "9882d6d1-e230-418d-a751-4924db03fcc1": { + "id": "9882d6d1-e230-418d-a751-4924db03fcc1", + "alias": "artifact_9882d6d1e230418da7514924db03fcc1", + "name": "SA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "9b07222e-132a-4505-a40e-5761277e43b4", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + }, + "f3635689-622e-4e71-849a-ec332f8931f3": { + "id": "f3635689-622e-4e71-849a-ec332f8931f3", + "alias": "artifact_f3635689622e4e71849aec332f8931f3", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "05617eb5-8a1b-4c79-a957-d89567939995", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + }, + "traces": { + "f27b20ea-1a66-4a91-87d6-79b7bda09a13": { + "id": "f27b20ea-1a66-4a91-87d6-79b7bda09a13", + "alias": "artifact_review", + "name": "Interlock requirement rationale", + "description": "", + "oa": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "sa": "9b07222e-132a-4505-a40e-5761277e43b4", + "kind": "Manual", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json new file mode 100644 index 00000000..6e691d2d --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.manifest.json @@ -0,0 +1,20 @@ +{ + "fixture": "semantic-artifacts-delete", + "schema": 6, + "project": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "trace_ids": [ + "f27b20ea-1a66-4a91-87d6-79b7bda09a13" + ], + "candidate_ids": [], + "initial_artifact_ids": [ + "9882d6d1-e230-418d-a751-4924db03fcc1", + "f3635689-622e-4e71-849a-ec332f8931f3" + ], + "final_artifact_ids": [ + "f3635689-622e-4e71-849a-ec332f8931f3" + ], + "acknowledged_count": 8, + "retired_count": 0, + "impact_count": 0, + "native_acceptance": "pending" +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json new file mode 100644 index 00000000..ad1ca9d1 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-delete.request.json @@ -0,0 +1,49 @@ +{ + "transition": { + "direction": "OaToSa", + "sources": [ + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "target": "9b07222e-132a-4505-a40e-5761277e43b4", + "target_kind": null + } + ], + "fields": [], + "resolutions": [], + "destination_diagram": null, + "communication_endpoints": [] + }, + "traces": { + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": "f27b20ea-1a66-4a91-87d6-79b7bda09a13" + }, + "references": { + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": "9b07222e-132a-4505-a40e-5761277e43b4", + "f3635689-622e-4e71-849a-ec332f8931f3": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "reviews": [ + { + "source": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "resolutions": [ + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Presence" + } + }, + "decision": "TakeSource" + } + ], + "reset": [] + } + ], + "candidates": [], + "accepted_impacts": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia new file mode 100644 index 00000000..accef6a2 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.arcadia @@ -0,0 +1,115 @@ +// Exact artifact rationale: no implicit metadata review +arcadia 6; +project mission "Explicit requirement-link review" id "a1db88f7-0af1-4ce9-825e-2a407eaa647a" { + description ""; + + oa_activity oa_safety_coordination "OA Safety coordination" id "05617eb5-8a1b-4c79-a957-d89567939995" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "9b07222e-132a-4505-a40e-5761277e43b4" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "bd6e974f-1f72-4ed9-b146-72b272f4c861" { + description ""; + parent none; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" { + description ""; + parent none; + } + + artifact artifact_9882d6d1e230418da7514924db03fcc1 "SA Interlock requirement" id "9882d6d1-e230-418d-a751-4924db03fcc1" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment none; + owner @sa_safety_coordination; + relation Satisfies; + revision ""; + uri "requirements/interlock"; + } + + artifact artifact_f3635689622e4e71849aec332f8931f3 "OA Interlock requirement" id "f3635689-622e-4e71-849a-ec332f8931f3" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @oa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + trace artifact_review "Interlock requirement rationale" id "f27b20ea-1a66-4a91-87d6-79b7bda09a13" { + baseline { + oa { + description ""; + name "OA Startup coordination"; + }; + sa { + description ""; + name "SA Startup coordination"; + }; + semantic { + facets [{ + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Owner { + oa { + Value uuid "d4d778ce-95c7-4e83-82ba-d129c6b61dc9"; + }; + sa { + Value uuid "bd6e974f-1f72-4ed9-b146-72b272f4c861"; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Revision { + oa { + Value none; + }; + sa { + Value ""; + }; + }; + }; + }; + }, { + Artifact { + keys { + oa uuid "f3635689-622e-4e71-849a-ec332f8931f3"; + sa uuid "9882d6d1-e230-418d-a751-4924db03fcc1"; + }; + review { + Fragment { + oa { + Value "section-4"; + }; + sa { + Value none; + }; + }; + }; + }; + }]; + }; + }; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json new file mode 100644 index 00000000..c33f44e1 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.after.json @@ -0,0 +1,154 @@ +{ + "schema_version": 6, + "id": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "alias": "mission", + "name": "Explicit requirement-link review", + "description": "", + "elements": { + "05617eb5-8a1b-4c79-a957-d89567939995": { + "id": "05617eb5-8a1b-4c79-a957-d89567939995", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "9b07222e-132a-4505-a40e-5761277e43b4": { + "id": "9b07222e-132a-4505-a40e-5761277e43b4", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "bd6e974f-1f72-4ed9-b146-72b272f4c861": { + "id": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": { + "id": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": { + "9882d6d1-e230-418d-a751-4924db03fcc1": { + "id": "9882d6d1-e230-418d-a751-4924db03fcc1", + "alias": "artifact_9882d6d1e230418da7514924db03fcc1", + "name": "SA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": "", + "fragment": null + }, + "f3635689-622e-4e71-849a-ec332f8931f3": { + "id": "f3635689-622e-4e71-849a-ec332f8931f3", + "alias": "artifact_f3635689622e4e71849aec332f8931f3", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + }, + "traces": { + "f27b20ea-1a66-4a91-87d6-79b7bda09a13": { + "id": "f27b20ea-1a66-4a91-87d6-79b7bda09a13", + "alias": "artifact_review", + "name": "Interlock requirement rationale", + "description": "", + "oa": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "sa": "9b07222e-132a-4505-a40e-5761277e43b4", + "kind": "Manual", + "baseline": { + "oa": { + "name": "OA Startup coordination", + "description": "" + }, + "sa": { + "name": "SA Startup coordination", + "description": "" + }, + "semantic": { + "facets": [ + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Owner": { + "oa": { + "Value": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" + }, + "sa": { + "Value": "bd6e974f-1f72-4ed9-b146-72b272f4c861" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Revision": { + "oa": { + "Value": null + }, + "sa": { + "Value": "" + } + } + } + } + }, + { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "review": { + "Fragment": { + "oa": { + "Value": "section-4" + }, + "sa": { + "Value": null + } + } + } + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia new file mode 100644 index 00000000..f277c77c --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.arcadia @@ -0,0 +1,52 @@ +// Exact artifact rationale: no implicit metadata review +arcadia 6; +project mission "Explicit requirement-link review" id "a1db88f7-0af1-4ce9-825e-2a407eaa647a" { + description ""; + + oa_activity oa_safety_coordination "OA Safety coordination" id "05617eb5-8a1b-4c79-a957-d89567939995" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "9b07222e-132a-4505-a40e-5761277e43b4" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "bd6e974f-1f72-4ed9-b146-72b272f4c861" { + description ""; + parent none; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" { + description ""; + parent none; + } + + artifact artifact_9882d6d1e230418da7514924db03fcc1 "SA Interlock requirement" id "9882d6d1-e230-418d-a751-4924db03fcc1" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @sa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + artifact artifact_f3635689622e4e71849aec332f8931f3 "OA Interlock requirement" id "f3635689-622e-4e71-849a-ec332f8931f3" { + description "Reviewed safety rationale\r\nSecond exact line"; + fragment "section-4"; + owner @oa_startup_coordination; + relation Satisfies; + revision none; + uri "requirements/interlock"; + } + + trace artifact_review "Interlock requirement rationale" id "f27b20ea-1a66-4a91-87d6-79b7bda09a13" { + baseline none; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json new file mode 100644 index 00000000..5f2685cb --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.json @@ -0,0 +1,87 @@ +{ + "schema_version": 6, + "id": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "alias": "mission", + "name": "Explicit requirement-link review", + "description": "", + "elements": { + "05617eb5-8a1b-4c79-a957-d89567939995": { + "id": "05617eb5-8a1b-4c79-a957-d89567939995", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "9b07222e-132a-4505-a40e-5761277e43b4": { + "id": "9b07222e-132a-4505-a40e-5761277e43b4", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "bd6e974f-1f72-4ed9-b146-72b272f4c861": { + "id": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": { + "id": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": { + "9882d6d1-e230-418d-a751-4924db03fcc1": { + "id": "9882d6d1-e230-418d-a751-4924db03fcc1", + "alias": "artifact_9882d6d1e230418da7514924db03fcc1", + "name": "SA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "9b07222e-132a-4505-a40e-5761277e43b4", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + }, + "f3635689-622e-4e71-849a-ec332f8931f3": { + "id": "f3635689-622e-4e71-849a-ec332f8931f3", + "alias": "artifact_f3635689622e4e71849aec332f8931f3", + "name": "OA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": null, + "fragment": "section-4" + } + }, + "traces": { + "f27b20ea-1a66-4a91-87d6-79b7bda09a13": { + "id": "f27b20ea-1a66-4a91-87d6-79b7bda09a13", + "alias": "artifact_review", + "name": "Interlock requirement rationale", + "description": "", + "oa": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "sa": "9b07222e-132a-4505-a40e-5761277e43b4", + "kind": "Manual", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json new file mode 100644 index 00000000..7d58a137 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.manifest.json @@ -0,0 +1,23 @@ +{ + "fixture": "semantic-artifacts-move", + "schema": 6, + "project": "a1db88f7-0af1-4ce9-825e-2a407eaa647a", + "trace_ids": [ + "f27b20ea-1a66-4a91-87d6-79b7bda09a13" + ], + "candidate_ids": [ + "9882d6d1-e230-418d-a751-4924db03fcc1" + ], + "initial_artifact_ids": [ + "9882d6d1-e230-418d-a751-4924db03fcc1", + "f3635689-622e-4e71-849a-ec332f8931f3" + ], + "final_artifact_ids": [ + "9882d6d1-e230-418d-a751-4924db03fcc1", + "f3635689-622e-4e71-849a-ec332f8931f3" + ], + "acknowledged_count": 3, + "retired_count": 0, + "impact_count": 0, + "native_acceptance": "pending" +} diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json new file mode 100644 index 00000000..22385417 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-move.request.json @@ -0,0 +1,87 @@ +{ + "transition": { + "direction": "OaToSa", + "sources": [ + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "target": "9b07222e-132a-4505-a40e-5761277e43b4", + "target_kind": null + } + ], + "fields": [], + "resolutions": [], + "destination_diagram": null, + "communication_endpoints": [] + }, + "traces": { + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": "f27b20ea-1a66-4a91-87d6-79b7bda09a13" + }, + "references": { + "d4d778ce-95c7-4e83-82ba-d129c6b61dc9": "9b07222e-132a-4505-a40e-5761277e43b4", + "f3635689-622e-4e71-849a-ec332f8931f3": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "reviews": [ + { + "source": "d4d778ce-95c7-4e83-82ba-d129c6b61dc9", + "resolutions": [ + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Owner" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Revision" + } + }, + "decision": "Candidate" + }, + { + "facet": { + "Artifact": { + "keys": { + "oa": "f3635689-622e-4e71-849a-ec332f8931f3", + "sa": "9882d6d1-e230-418d-a751-4924db03fcc1" + }, + "facet": "Fragment" + } + }, + "decision": "Candidate" + } + ], + "reset": [] + } + ], + "candidates": [ + { + "Artifact": { + "id": "9882d6d1-e230-418d-a751-4924db03fcc1", + "alias": "artifact_9882d6d1e230418da7514924db03fcc1", + "name": "SA Interlock requirement", + "description": "Reviewed safety rationale\r\nSecond exact line", + "owner": "bd6e974f-1f72-4ed9-b146-72b272f4c861", + "relation": "Satisfies", + "uri": "requirements/interlock", + "revision": "", + "fragment": null + } + } + ], + "accepted_impacts": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md b/harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md new file mode 100644 index 00000000..55dbaf15 --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts-native.md @@ -0,0 +1,37 @@ +# Artifact semantic native fixtures + +All three variants have **Interlock requirement rationale** as the owner trace. Open Review semantics > Artifact links. Work on copies of the original `.arcadia` files. Exact before/after JSON, source, and core requests sit beside each fixture. + +## Move and optional metadata + +Open `semantic-artifacts-move.arcadia`. Pair **OA Interlock requirement** and **SA Interlock requirement**, then Review this exact artifact pair. Select these three properties independently with Set final value: + +- Owner: **SA Safety coordination**. +- Revision: Present checked, empty text. This is `Some("")`. +- Fragment: Present unchecked. The disabled `section-4` text remains a draft; accepted value is `None`. + +Before Apply, Keep Presence must produce an explicit conflict with the Owner move. Leave that Presence decision unselected to restore valid preview. Type `-` into Owner identity, switch rows/tabs, checkpoint and recover; rejected raw input must remain. Repair the Owner identity with the named picker. Apply once and compare exactly with `semantic-artifacts-move.after.json/.after.arcadia`. Undo/Redo once restores exact source/model/history. The link moves, so subsequent metadata review requires an appropriate trace at the new owner; the old trace must not silently rebind. + +## Source attachment absence and deletion + +Open `semantic-artifacts-delete.arcadia`. Its OA link still exists, now attached to **OA Safety coordination**. Review the original trace and explicitly pair the historical OA link with the SA link. The OA picker directly offers this same-layer link, labeled with its current Safety owner and “not attached to reviewed owner”. Take Artifact Presence: the SA link is removed while the OA link remains. The exact after oracle contains eight acknowledged facts (source metadata still exists), not seven retired facts. A genuinely deleted source link would retire unavailable metadata instead, as covered by the core test. + +**No OA identity** means destination-only retention; it must not act as deletion. No fake missing UUID or candidate record is used as a tombstone. + +## Creation and provisional identity + +Open `semantic-artifacts-create.arcadia`. Choose **OA Interlock requirement**, then Reserve a new destination counterpart, then Review this exact artifact pair. Take Presence alone remains blocked. Deliberately review Name, Description, Owner, Relation, URI, Revision and Fragment, using Set final value for every field. The seeded values are from the source, with the destination owner and a fresh reserved identity. + +The GUI deliberately reserves a random stable UUID, so the initially supplied creation after oracle is a template. Obtain the reserved UUID from its saved recovery seed or the created artifact after Apply, then regenerate the exact oracle against the unchanged fixture base: + +`. .local/toolchain/env.ps1` + +`.local/native-fixtures/generate-semantic-artifacts-preserved.exe .local/native-fixtures/artifacts-session ` + +Compile the preserved generator using this directory’s README. This mode reads the existing `semantic-artifacts-create.json` in the explicitly supplied copied directory, preserves that fixture identity, and updates only the request/after oracle using the explicit reserved ID. It must not be called without the UUID after native work, since output-directory-only generation creates new initial fixture identities. + +Compare accepted source/model/history exactly. This creation gesture has eight explicit reviews and one complete candidate. Checkpoint/recover before Apply must retain the reserved identity, per-field drafts and excluded state; reservation itself must not add a model record. + +## Layout checks + +At 1100x700 and 1440x900, verify wrapping tabs, pair picker actions, description editor, Owner picker/raw field and optional text toggles. All row actions must remain outside the right scrollbar gutter. Native execution is pending; the generator only validates direct-core preparation and exact source round trips. diff --git a/harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json b/harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json new file mode 100644 index 00000000..5060af0c --- /dev/null +++ b/harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json @@ -0,0 +1,26 @@ +{ + "algorithm": "SHA256", + "files": { + "generate-semantic-artifacts.rs": "444f360b78df52c5e42046f77b148781e0c9bddc2a09c0b7bd65c7a4f376cfe6", + "README.md": "24200c84ccc2ef3e17c029744074e5cea785188f5549ac54e44cf89682643f81", + "semantic-artifacts-create.after.arcadia": "84b22e95c213018612e6e8a84cad68b35a8b495ff3515b0f978c5e7a3d196d79", + "semantic-artifacts-create.after.json": "13a6f99ec2df1dd35ee8b2613f183918cd9537eda4a5da085ad52e65c033db59", + "semantic-artifacts-create.arcadia": "1e0d662ba33071b122ad9206d2d559a8e748c9431eb3ac1b759bde3760ca877c", + "semantic-artifacts-create.json": "fa6a2e3643c0f909b92407abec3edc897033162eeaa501f8d4616f79d41fa044", + "semantic-artifacts-create.manifest.json": "65cb70b0d697a055363ba01bf30bed7e412d43c9978a033549c378d117719ae7", + "semantic-artifacts-create.request.json": "ed905267263e7f8a0cd2dbe833196b30118b6a246815216932fae661ad650699", + "semantic-artifacts-delete.after.arcadia": "e50fc462ec22dbf98011929931dd2489430001c3e6433608685b9d88bceb9acb", + "semantic-artifacts-delete.after.json": "f27a7b38d59c7cefe58cb36783d3d4dd8d8ae1168d9aed1eb61d343c18758908", + "semantic-artifacts-delete.arcadia": "eba2aec39e6efb405eace4bc8ab0c5563907c26d449e71a4aa750df9f73233b4", + "semantic-artifacts-delete.json": "2ef777f19b94f08b5e5ca2ef727777897e1018f1426eee291408b3c69101f596", + "semantic-artifacts-delete.manifest.json": "3af7232ed3056ca84dcaf0f266ead3faaa512a0dc0051c7ba68791c20dbae0cf", + "semantic-artifacts-delete.request.json": "8af21b00b62e620256fbf50e47c6c5836cdb6922d9549cc4b3d86bc03b008073", + "semantic-artifacts-move.after.arcadia": "20bb30473830e44228958e78fb7480269cdcc0580c6c90c718da75f34ca5a43f", + "semantic-artifacts-move.after.json": "44f4b05db49758dcf344788f57cecc0250fd029099b1c54e4e1e3e972f6cf039", + "semantic-artifacts-move.arcadia": "b0fd031671c9f755756922ec5c40a0ce3f40164d22c3c389aec9fd0705344da7", + "semantic-artifacts-move.json": "1acf81e08c51c85aceb14a3efbc39ec680552a19c7824aab4611ac3cf71b9c6d", + "semantic-artifacts-move.manifest.json": "a07978f4b0a3ca83e5ae5f35e7b66b26473b24431fc90e5889d755f1816d52dd", + "semantic-artifacts-move.request.json": "5ff844a038b0030adb5a83d13d81e60f30627543388f6199b6a0e026fc42ea58", + "semantic-artifacts-native.md": "a51ffbc45b6c172cbfed3b16c249296e6a28d457fea57393dd958cdcfbafbca1" + } +} diff --git a/harness/fixtures/release1-native/communication-means/README.md b/harness/fixtures/release1-native/communication-means/README.md new file mode 100644 index 00000000..ad1e1ff3 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/README.md @@ -0,0 +1,220 @@ +# Communication means native QA fixtures + +**Acceptance remains scoped by the native reports.** The actual drawn baseline +has independently passed the draw+rename oracle below; this does not accept every +transition variant. These deterministic source-6 fixtures and direct +core oracles cover participant communication, explicit SA endpoint choices and a +reverse boundary Call/correlated Reply. They do not simulate native drawing or +establish readability. Root is the native operator. Work on isolated copies; +never overwrite the tracked bases with native saves. + +## Fixtures and expected behavior + +- `cm-draw.arcadia`: OA **Rescue team** / **Control centre**, independently owned + activities **Report rescue status** / **Assess rescue status**, two allocations + and **Rescue status** Interaction. There is **no CommunicationMean**. Open the + saved **Draw operational communication** Overview: participant rectangles + `(80,70,250,160)` / `(550,70,250,160)`, work rectangles at y330; horizontal gap + 220. SA **Rescue radio** / **Control radio** already have one Standard/Unset + component socket each, and explicit participant traces. No interfaces, + PortAllocations or ExchangeAllocations are invented. +- `cm-transition-ready.arcadia`: same base plus unoriented **Rescue radio channel** + from Rescue team to Control centre. This isolates transition QA if drawing is + investigated separately. Existing traces are **Team radio correspondence** and + **Control radio correspondence**. +- `cm-reuse.after-template/entry.arcadia`: both SA sockets explicitly reused; + no new element, one BoundaryExchange and its Exchange trace added. +- `cm-create.after-template/entry.arcadia`: source **Existing team socket** reused; + destination explicitly creates a Standard/Unset port under Control radio. + Exactly one new element, one BoundaryExchange and its Exchange trace added; + **Existing control socket** is retained unused. Created name follows the GUI: + `Rescue radio channel · target port`, empty description. +- `cm-reverse-scenario.arcadia`: existing mapped unoriented channel/boundary plus + SA BoundaryInterface scenario **Reverse rescue request**. **Control radio + lifeline** calls **Team radio lifeline**, opposite the stored boundary endpoints; + **Reply with rescue report** returns and explicitly correlates to **Request + rescue report**. All ports are Standard/Unset. +- `cm-reverse-scenario.after-template/entry.arcadia`: **legacy desktop policy** whole-subtree SA→OA transition + preserves the existing source scenario/messages, maps the boundary to the + existing OA mean and participants, creates the five OA scenario/lifeline/ + occurrence counterparts and five traces, and creates **zero OA ports**. + Call resolves Control centre→Rescue team; Reply resolves Rescue team→Control + centre and points to the mapped Call identity. Legacy desktop Apply also adds five + placements to the existing OA diagram. The oracle independently predicts all + five placements, rather than discarding presentation differences. + +Template after directories are **complete source bundles**, including generated +`traceability.arcadia` and other modules. Copy entire directories. Their fresh +counterpart/trace IDs are deterministic reference IDs, not the UUIDs a native +transition will allocate. `manifest.json` supplies all stable base identities. +Each request JSON is a complete core request; `destination_diagram` is None. +Under the legacy desktop policy used by these original templates, this does not +disable placement: the GUI selects the first target-layer diagram and places +changed elements that are not already shown. The corrected behavior-root policy +uses the separate `reverse-behavior` mode below and preserves every saved view. + +## Native steps + +1. Open a copy of `cm-draw.arcadia`, select Operational / **Draw operational + communication**. Fit, then use working zoom at 1440×900 and 1100×700. Start + **Communication means**, click Rescue team then Control centre. Confirm the + endpoint tool excludes Activity targets and creates one participant-level + relation without OA ports. Rename it **Rescue radio channel**. Confirm the + channel is **unoriented** (`oriented false`), with no items. Save this complete before-transition + project in a new trial directory; preserve its entire source bundle for Undo. + The Interaction and its existing allocations must remain unchanged. +2. Select the mean and open transition to System. **Keep the initial Required + references scope while explicitly selecting** Rescue team→Rescue radio and + Control centre→Control radio in their participant pickers. Seeing an existing + trace candidate is not the same as authoring its mapping. The original + instructions switched to Selected only too early, hiding both participant + rows and their existing port choices. This was an instruction error. + Once both mappings are authored, switch **Include → Selected only**; mapping + and endpoint drafts survive the scope change. This preserves the selected-only + request's exact boundary: participant names/baselines are not updated. If + target Name review is shown while Required references is selected, Keep target + is consistent; it need not appear after switching. Before both communication + endpoints are chosen, Apply must remain blocked. +3. In **System communication endpoints**, source uses **Existing team socket**. + Target uses **Create Standard port · bidirectional**. Verify the new port is + under Control radio and no existing socket disappears. Do not change other + port metadata. Selected-only forward creation adds no standalone node placement: + the exchange is the plan change and the component port belongs to its owner. + One Apply then Save As must match the `check create` oracle below. One Undo/Save + must equal the full captured before bundle; one Redo/Save must equal the after + bundle exactly, retaining generated UUIDs. +4. In a separate clean copy, repeat with both **Existing team socket** and + **Existing control socket**. Run `check reuse`. There must be no new port. + These are alternative trials, not successive edits to one counterpart. +5. **Legacy binary reproduction:** open a copy of `cm-reverse-scenario.arcadia`. In System Behavior select + **Reverse rescue request**. Inspect both named lifelines, reverse Call and + correlated Reply. Transition the **scenario** to Operational with **Whole + subtree**; map the boundary to the existing Rescue radio channel and both + participants to their existing OA counterparts. Keep target Names for differing + participants. The legacy GUI automatically places new target elements in its first + OA diagram; no placement opt-out is exposed. One Apply/Save must pass + `check reverse`; inspect operational lifelines, mapped Reply-to identity, + both message directions and absence of OA ports. Repeat exact Undo/Redo saves. + +If a UI choice produces a different result, preserve the full trial bundle and +record the exact choice. Do not modify the expected contract to make it pass. +Exchange allocation authoring, Flow-port directional refusal, crash recovery and +other communication transition variants are separate gates unless exercised. + +## Generator and exact oracle checker + +Compile outside Cargo/shared target, after the owner confirms these libraries +are stable. The matching serde_json fingerprint matters; another feature graph +with the same crate version will fail Rust trait identity checks. + +```powershell +. .local/toolchain/env.ps1 +rustc --edition 2024 -C linker=clang.exe --extern oa_core=.local/target-gnullvm/debug/deps/liboa_core-6c8da57367eec642.rlib --extern serde_json=.local/target-gnullvm/debug/deps/libserde_json-da2c4f333fc47818.rlib -L dependency=.local/target-gnullvm/debug/deps harness/fixtures/release1-native/communication-means/generate-communication-means.rs -o .local/native-fixtures/generate-communication-means.exe +.local/native-fixtures/generate-communication-means.exe generate .local/native-fixtures/cm-fresh +.local/native-fixtures/generate-communication-means.exe check create BEFORE_ENTRY.arcadia AFTER_ENTRY.arcadia .local/native-fixtures/cm-native-oracle +``` + +Use `reuse` or `reverse` instead of `create` for the other cases. The checker loads +real complete source bundles. It constructs the independent expected core request +from the preserved before model, with the specified choices above. Actual output +supplies **only** the created port UUID (create trial) and the new target/trace UUIDs +matched by exact source identity. New placement UUIDs are matched only by exact +destination diagram and mapped element identity. Typed reference visitors plus identity-field +rekeying perform those substitutions; no names, aliases, metadata, history, +orientations, bindings or reply fields are copied from actual output. Existing +identities cannot be normalized away. Placement order/geometry is independently +predicted from the plan's changes and prior diagram: starting with four existing +OA nodes, positions are `(340,210)`, `(620,210)`, `(60,360)`, `(340,360)`, +`(620,360)`, each size224×94, unfolded with no port anchors. Existing placements +remain unchanged. Names/aliases follow the same core placement primitive, with +no observed native metadata copied into the expected result. The complete resulting Project must equal +actual, then every reachable source module must equal the production updater's +output byte-for-byte. Only the entry *filename* is aligned for Save As; contents +and all included paths remain exact. Successful checking writes expected JSON, +the complete expected bundle and native request into the supplied output folder. + +Direct-rustc validation passed generation, complete JSON/source round trips and +all three checker modes against their template bundles. Forward modes substitute +two generated target/trace UUIDs and no placement IDs; reverse substitutes ten +target/trace and five placement UUIDs. All six initial source/JSON fixture files +remain byte-for-byte unchanged by this correction. Regenerating every output in +a fresh directory was byte-exact. Temporary counterfactuals with changed placement +x coordinate, width, alias or swapped element assignments were each rejected by +the full-model oracle; an added source newline was separately rejected by exact +bundle comparison. The earlier changed-name after bundle was also rejected. +Ignored trial evidence is under `.local/native-fixtures/cm-placement-falsifiers/`. +These are core/tool checks, not native passes. +Core test contracts reused: `communication_transition_requires_explicit_ports_and_preserves_participant_mapping`, +`communication_reverse_transition_projects_owners_without_oa_ports`, and +`reverse_boundary_scenario_transition_keeps_call_orientation_and_reply_identity`. + +## Actual native drawing baseline oracle + +`check draw BEFORE_ENTRY AFTER_ENTRY OUTPUT_DIRECTORY` constructs an independent +`CreateRelation` using the GUI's initial engineering name/alias rule, then an +`EditRelation` changing only the name to **Rescue radio channel**. The production +source updater runs after each command. Actual output supplies only the one new +relation UUID; the checker compares the complete Project and every reachable +source byte, aligning only the entry filename for Save As. + +Actual `.local/native-qa/cm-create-before/entry.arcadia` passed against tracked +`cm-draw.arcadia`. The new UUID is `3e576659-3455-4fb5-811e-5f4a326948f4`; expected +alias is `communication_means_rescue_team_control_centre`, empty description/items, +`oriented false`, endpoints cm10→cm11. The complete native entry and independent +expected entry both hash to +`40363373B2443714F38D5825F8FAF53183CF004962059E0B611D01DB8E7F7F7E`. +Log: `.local/native-fixtures/cm-native-draw-check.log`; full expected model/source +and serialized commands: `.local/native-fixtures/cm-native-draw-oracle/`. +Changed orientation and alias were independently rejected by full-model equality; +added source whitespace was rejected by exact-source comparison. The native files +and existing tracked fixture/oracle models were read-only during this verification. + +```powershell +. .local/toolchain/env.ps1 +.local/native-fixtures/generate-communication-means.exe check draw harness/fixtures/release1-native/communication-means/cm-draw.arcadia .local/native-qa/cm-create-before/entry.arcadia .local/native-fixtures/cm-native-draw-oracle +``` + +## Behavior navigation policy: separate reverse oracle + +The original `check reverse` and `cm-reverse-scenario.after-template` remain the +**legacy automatic-placement oracle**: ten target/trace UUIDs plus five placement +UUIDs. Root verified that policy natively, including exact Save/Undo/Redo. It is +retained to explain the earlier binary's accepted result, not silently revised. + +For a binary containing the behavior-transition navigation correction, repeat the +same reverse scenario review from the unchanged `cm-reverse-scenario.arcadia` +baseline. After Apply, Behavior should show the explicit OA counterpart of +**Reverse rescue request**, with the Call and correlated Reply. It must not switch +to an unrelated architecture view or add placements there. All saved diagrams, +including geometry, routing and metadata, must remain exactly equal to baseline. +Save, Undo/save and Redo/save remain one complete transition operation. + +Use the distinct checker (replace `NEW_NATIVE_AFTER_ENTRY` with the saved entry): + +```powershell +. .local/toolchain/env.ps1 +.local/native-fixtures/generate-communication-means.exe check reverse-behavior harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia NEW_NATIVE_AFTER_ENTRY .local/native-fixtures/cm-native-reverse-behavior-oracle +``` + +`cm-reverse-behavior.after-template` is the new complete expected bundle. Its +generation mode takes the original source bundle and uses the production source +updater; it does not rewrite the legacy fixture or templates: + +```powershell +.local/native-fixtures/generate-communication-means.exe generate-reverse-behavior harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia NEW_OUTPUT_DIRECTORY +``` + +The new policy calls the shared core planner/application with the explicit +scenario root and no destination diagram, independently asserts unchanged +diagrams, and preserves existing reverse Call/Reply/binding/no-OA-port assertions. +Only ten generated target/trace UUIDs are normalized; no placement identity or +property is copied from native output. Full Project and complete source-byte +comparisons still apply. This mode covers this explicit scenario fixture, not +arbitrary behavior root types or explicit destination requests. + +Direct-rustc checks passed the new template and the unchanged legacy template. +The new checker rejected the legacy placements; the legacy checker rejected +their omission; the new checker also rejected an added source newline. Regeneration +was byte-exact. Negative logs are in +`.local/native-fixtures/cm-reverse-behavior-falsifiers/`. These are fixture-tool +checks; native acceptance of the corrected navigation policy remains pending. diff --git a/harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia b/harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia new file mode 100644 index 00000000..3fe14043 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-create.after-template/entry.arcadia @@ -0,0 +1,244 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + + sa_port rescue_radio_channel_target_port "Rescue radio channel · target port" id "c6060906-0000-4000-8000-000000001f40" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + + } + + + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + communication_mean cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000000032" { + description ""; + items []; + oriented false; + source @cm_10; + target @cm_11; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline none; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline none; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + + + boundary_exchange sa_cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000001f41" { + description ""; + items []; + oriented false; + source @cm_32; + target @rescue_radio_channel_target_port; + } + + + + include "traceability.arcadia"; +} diff --git a/harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json b/harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json new file mode 100644 index 00000000..418e3c93 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-create.after-template/expected.json @@ -0,0 +1,370 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f40": { + "id": "c6060906-0000-4000-8000-000000001f40", + "alias": "rescue_radio_channel_target_port", + "name": "Rescue radio channel · target port", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000000032": { + "id": "c6060906-0000-4000-8000-000000000032", + "alias": "cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "CommunicationMean": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000b", + "oriented": false, + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000001f41": { + "id": "c6060906-0000-4000-8000-000000001f41", + "alias": "sa_cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "BoundaryExchange": { + "source": "c6060906-0000-4000-8000-000000000020", + "target": "c6060906-0000-4000-8000-000000001f40", + "oriented": false, + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000001f42": { + "id": "c6060906-0000-4000-8000-000000001f42", + "alias": "trace_exchange_exchange", + "name": "Rescue radio channel ↔ Rescue radio channel", + "description": "", + "oa": "c6060906-0000-4000-8000-000000000032", + "sa": "c6060906-0000-4000-8000-000000001f41", + "kind": "Exchange", + "baseline": { + "oa": { + "name": "Rescue radio channel", + "description": "" + }, + "sa": { + "name": "Rescue radio channel", + "description": "" + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia b/harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia new file mode 100644 index 00000000..e55f64d8 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-create.after-template/traceability.arcadia @@ -0,0 +1,19 @@ + +trace trace_exchange_exchange "Rescue radio channel ↔ Rescue radio channel" id "c6060906-0000-4000-8000-000000001f42" { + baseline { + oa { + description ""; + name "Rescue radio channel"; + }; + sa { + description ""; + name "Rescue radio channel"; + }; + }; + description ""; + kind Exchange; + oa @cm_50; + sa @sa_cm_50; +} + + diff --git a/harness/fixtures/release1-native/communication-means/cm-create.request.json b/harness/fixtures/release1-native/communication-means/cm-create.request.json new file mode 100644 index 00000000..05e99970 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-create.request.json @@ -0,0 +1,56 @@ +{ + "direction": "OaToSa", + "sources": [ + "c6060906-0000-4000-8000-000000000032" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000001e", + "target_kind": null + }, + { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000001f", + "target_kind": null + } + ], + "fields": [ + "Name", + "Description" + ], + "resolutions": [ + { + "source": "c6060906-0000-4000-8000-00000000000a", + "field": "Name", + "decision": "KeepTarget" + }, + { + "source": "c6060906-0000-4000-8000-00000000000b", + "field": "Name", + "decision": "KeepTarget" + } + ], + "destination_diagram": null, + "communication_endpoints": [ + { + "source": "c6060906-0000-4000-8000-000000000032", + "source_port": { + "Reuse": { + "port": "c6060906-0000-4000-8000-000000000020" + } + }, + "target_port": { + "Create": { + "id": "c6060906-0000-4000-8000-00000000003c", + "alias": "rescue_radio_channel_target_port", + "name": "Rescue radio channel · target port", + "description": "", + "kind": "Standard", + "orientation": "Unset" + } + } + } + ] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-draw.arcadia b/harness/fixtures/release1-native/communication-means/cm-draw.arcadia new file mode 100644 index 00000000..54fe8bb5 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-draw.arcadia @@ -0,0 +1,210 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline none; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline none; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + +} diff --git a/harness/fixtures/release1-native/communication-means/cm-draw.json b/harness/fixtures/release1-native/communication-means/cm-draw.json new file mode 100644 index 00000000..7f8b6e91 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-draw.json @@ -0,0 +1,301 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia b/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia new file mode 100644 index 00000000..f6a13a0b --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/entry.arcadia @@ -0,0 +1,230 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + communication_mean cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000000032" { + description ""; + items []; + oriented false; + source @cm_10; + target @cm_11; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline none; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline none; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + + + boundary_exchange sa_cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000001f40" { + description ""; + items []; + oriented false; + source @cm_32; + target @cm_33; + } + + + + include "traceability.arcadia"; +} diff --git a/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json b/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json new file mode 100644 index 00000000..182b0b4d --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/expected.json @@ -0,0 +1,348 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000000032": { + "id": "c6060906-0000-4000-8000-000000000032", + "alias": "cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "CommunicationMean": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000b", + "oriented": false, + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000001f40": { + "id": "c6060906-0000-4000-8000-000000001f40", + "alias": "sa_cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "BoundaryExchange": { + "source": "c6060906-0000-4000-8000-000000000020", + "target": "c6060906-0000-4000-8000-000000000021", + "oriented": false, + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000001f41": { + "id": "c6060906-0000-4000-8000-000000001f41", + "alias": "trace_exchange_exchange", + "name": "Rescue radio channel ↔ Rescue radio channel", + "description": "", + "oa": "c6060906-0000-4000-8000-000000000032", + "sa": "c6060906-0000-4000-8000-000000001f40", + "kind": "Exchange", + "baseline": { + "oa": { + "name": "Rescue radio channel", + "description": "" + }, + "sa": { + "name": "Rescue radio channel", + "description": "" + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia b/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia new file mode 100644 index 00000000..9dadeaa7 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reuse.after-template/traceability.arcadia @@ -0,0 +1,19 @@ + +trace trace_exchange_exchange "Rescue radio channel ↔ Rescue radio channel" id "c6060906-0000-4000-8000-000000001f41" { + baseline { + oa { + description ""; + name "Rescue radio channel"; + }; + sa { + description ""; + name "Rescue radio channel"; + }; + }; + description ""; + kind Exchange; + oa @cm_50; + sa @sa_cm_50; +} + + diff --git a/harness/fixtures/release1-native/communication-means/cm-reuse.request.json b/harness/fixtures/release1-native/communication-means/cm-reuse.request.json new file mode 100644 index 00000000..3cb990c8 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reuse.request.json @@ -0,0 +1,51 @@ +{ + "direction": "OaToSa", + "sources": [ + "c6060906-0000-4000-8000-000000000032" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000001e", + "target_kind": null + }, + { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000001f", + "target_kind": null + } + ], + "fields": [ + "Name", + "Description" + ], + "resolutions": [ + { + "source": "c6060906-0000-4000-8000-00000000000a", + "field": "Name", + "decision": "KeepTarget" + }, + { + "source": "c6060906-0000-4000-8000-00000000000b", + "field": "Name", + "decision": "KeepTarget" + } + ], + "destination_diagram": null, + "communication_endpoints": [ + { + "source": "c6060906-0000-4000-8000-000000000032", + "source_port": { + "Reuse": { + "port": "c6060906-0000-4000-8000-000000000020" + } + }, + "target_port": { + "Reuse": { + "port": "c6060906-0000-4000-8000-000000000021" + } + } + } + ] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia new file mode 100644 index 00000000..30e2f22f --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia @@ -0,0 +1,320 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + sa_scenario cm_200 "Reverse rescue request" id "c6060906-0000-4000-8000-0000000000c8" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@cm_203, @cm_204]; + viewpoint BoundaryInterface; + } + + sa_lifeline cm_201 "Team radio lifeline" id "c6060906-0000-4000-8000-0000000000c9" { + description ""; + parent @cm_200; + represented @cm_30; + } + + sa_lifeline cm_202 "Control radio lifeline" id "c6060906-0000-4000-8000-0000000000ca" { + description ""; + parent @cm_200; + represented @cm_31; + } + + sa_occurrence cm_203 "Request rescue report" id "c6060906-0000-4000-8000-0000000000cb" { + body { + Exchange { + exchange @sa_cm_50; + message_kind Call; + receiver @cm_201; + reply_to none; + sender @cm_202; + values []; + }; + }; + description ""; + parent @cm_200; + postcondition ""; + precondition ""; + } + + sa_occurrence cm_204 "Reply with rescue report" id "c6060906-0000-4000-8000-0000000000cc" { + body { + Exchange { + exchange @sa_cm_50; + message_kind Reply; + receiver @cm_202; + reply_to @cm_203; + sender @cm_201; + values []; + }; + }; + description ""; + parent @cm_200; + postcondition ""; + precondition ""; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + communication_mean cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000000032" { + description ""; + items []; + oriented false; + source @cm_10; + target @cm_11; + } + + boundary_exchange sa_cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000001f40" { + description ""; + items []; + oriented false; + source @cm_32; + target @cm_33; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline { + oa { + description ""; + name "Rescue team"; + }; + sa { + description ""; + name "Rescue radio"; + }; + }; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline { + oa { + description ""; + name "Control centre"; + }; + sa { + description ""; + name "Control radio"; + }; + }; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + + trace trace_exchange_exchange "Rescue radio channel ↔ Rescue radio channel" id "c6060906-0000-4000-8000-000000001f41" { + baseline { + oa { + description ""; + name "Rescue radio channel"; + }; + sa { + description ""; + name "Rescue radio channel"; + }; + }; + description ""; + kind Exchange; + oa @cm_50; + sa @sa_cm_50; + } + + + include "oa/behavior/scenario_oa_cm_200.arcadia"; + include "traceability.arcadia"; +} diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json new file mode 100644 index 00000000..458e0f08 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/expected.json @@ -0,0 +1,679 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000c8": { + "id": "c6060906-0000-4000-8000-0000000000c8", + "alias": "cm_200", + "name": "Reverse rescue request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Scenario": { + "viewpoint": "BoundaryInterface", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "c6060906-0000-4000-8000-0000000000cb", + "c6060906-0000-4000-8000-0000000000cc" + ] + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000c9": { + "id": "c6060906-0000-4000-8000-0000000000c9", + "alias": "cm_201", + "name": "Team radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000001e" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000ca": { + "id": "c6060906-0000-4000-8000-0000000000ca", + "alias": "cm_202", + "name": "Control radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000001f" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000cb": { + "id": "c6060906-0000-4000-8000-0000000000cb", + "alias": "cm_203", + "name": "Request rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000001f40", + "sender": "c6060906-0000-4000-8000-0000000000ca", + "receiver": "c6060906-0000-4000-8000-0000000000c9", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000cc": { + "id": "c6060906-0000-4000-8000-0000000000cc", + "alias": "cm_204", + "name": "Reply with rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000001f40", + "sender": "c6060906-0000-4000-8000-0000000000c9", + "receiver": "c6060906-0000-4000-8000-0000000000ca", + "message_kind": "Reply", + "reply_to": "c6060906-0000-4000-8000-0000000000cb", + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f42": { + "id": "c6060906-0000-4000-8000-000000001f42", + "alias": "oa_cm_200", + "name": "Reverse rescue request", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "Scenario": { + "viewpoint": "ParticipantInteraction", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "c6060906-0000-4000-8000-000000001f45", + "c6060906-0000-4000-8000-000000001f46" + ] + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f43": { + "id": "c6060906-0000-4000-8000-000000001f43", + "alias": "oa_cm_201", + "name": "Team radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000000a" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f44": { + "id": "c6060906-0000-4000-8000-000000001f44", + "alias": "oa_cm_202", + "name": "Control radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000000b" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f45": { + "id": "c6060906-0000-4000-8000-000000001f45", + "alias": "oa_cm_203", + "name": "Request rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000000032", + "sender": "c6060906-0000-4000-8000-000000001f44", + "receiver": "c6060906-0000-4000-8000-000000001f43", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f46": { + "id": "c6060906-0000-4000-8000-000000001f46", + "alias": "oa_cm_204", + "name": "Reply with rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000000032", + "sender": "c6060906-0000-4000-8000-000000001f43", + "receiver": "c6060906-0000-4000-8000-000000001f44", + "message_kind": "Reply", + "reply_to": "c6060906-0000-4000-8000-000000001f45", + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000000032": { + "id": "c6060906-0000-4000-8000-000000000032", + "alias": "cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "CommunicationMean": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000b", + "oriented": false, + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000001f40": { + "id": "c6060906-0000-4000-8000-000000001f40", + "alias": "sa_cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "BoundaryExchange": { + "source": "c6060906-0000-4000-8000-000000000020", + "target": "c6060906-0000-4000-8000-000000000021", + "oriented": false, + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": { + "oa": { + "name": "Rescue team", + "description": "" + }, + "sa": { + "name": "Rescue radio", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": { + "oa": { + "name": "Control centre", + "description": "" + }, + "sa": { + "name": "Control radio", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f41": { + "id": "c6060906-0000-4000-8000-000000001f41", + "alias": "trace_exchange_exchange", + "name": "Rescue radio channel ↔ Rescue radio channel", + "description": "", + "oa": "c6060906-0000-4000-8000-000000000032", + "sa": "c6060906-0000-4000-8000-000000001f40", + "kind": "Exchange", + "baseline": { + "oa": { + "name": "Rescue radio channel", + "description": "" + }, + "sa": { + "name": "Rescue radio channel", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f47": { + "id": "c6060906-0000-4000-8000-000000001f47", + "alias": "trace_oa_cm_200_cm_200", + "name": "Reverse rescue request ↔ Reverse rescue request", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f42", + "sa": "c6060906-0000-4000-8000-0000000000c8", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Reverse rescue request", + "description": "" + }, + "sa": { + "name": "Reverse rescue request", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f48": { + "id": "c6060906-0000-4000-8000-000000001f48", + "alias": "trace_oa_cm_201_cm_201", + "name": "Team radio lifeline ↔ Team radio lifeline", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f43", + "sa": "c6060906-0000-4000-8000-0000000000c9", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Team radio lifeline", + "description": "" + }, + "sa": { + "name": "Team radio lifeline", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f49": { + "id": "c6060906-0000-4000-8000-000000001f49", + "alias": "trace_oa_cm_202_cm_202", + "name": "Control radio lifeline ↔ Control radio lifeline", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f44", + "sa": "c6060906-0000-4000-8000-0000000000ca", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Control radio lifeline", + "description": "" + }, + "sa": { + "name": "Control radio lifeline", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f4a": { + "id": "c6060906-0000-4000-8000-000000001f4a", + "alias": "trace_oa_cm_203_cm_203", + "name": "Request rescue report ↔ Request rescue report", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f45", + "sa": "c6060906-0000-4000-8000-0000000000cb", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Request rescue report", + "description": "" + }, + "sa": { + "name": "Request rescue report", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f4b": { + "id": "c6060906-0000-4000-8000-000000001f4b", + "alias": "trace_oa_cm_204_cm_204", + "name": "Reply with rescue report ↔ Reply with rescue report", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f46", + "sa": "c6060906-0000-4000-8000-0000000000cc", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Reply with rescue report", + "description": "" + }, + "sa": { + "name": "Reply with rescue report", + "description": "" + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia new file mode 100644 index 00000000..e71448e3 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia @@ -0,0 +1,63 @@ + +oa_scenario oa_cm_200 "Reverse rescue request" id "c6060906-0000-4000-8000-000000001f42" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@oa_cm_203, @oa_cm_204]; + viewpoint ParticipantInteraction; + + oa_lifeline oa_cm_201 "Team radio lifeline" id "c6060906-0000-4000-8000-000000001f43" { + description ""; + + represented @cm_10; + } + + + oa_lifeline oa_cm_202 "Control radio lifeline" id "c6060906-0000-4000-8000-000000001f44" { + description ""; + + represented @cm_11; + } + + + oa_occurrence oa_cm_203 "Request rescue report" id "c6060906-0000-4000-8000-000000001f45" { + body { + Exchange { + exchange @cm_50; + message_kind Call; + receiver @oa_cm_201; + reply_to none; + sender @oa_cm_202; + values []; + }; + }; + description ""; + + postcondition ""; + precondition ""; + } + + + oa_occurrence oa_cm_204 "Reply with rescue report" id "c6060906-0000-4000-8000-000000001f46" { + body { + Exchange { + exchange @cm_50; + message_kind Reply; + receiver @oa_cm_202; + reply_to @oa_cm_203; + sender @oa_cm_201; + values []; + }; + }; + description ""; + + postcondition ""; + precondition ""; + } + + +} + + diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json new file mode 100644 index 00000000..99740913 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/request.json @@ -0,0 +1,47 @@ +{ + "direction": "SaToOa", + "sources": [ + "c6060906-0000-4000-8000-0000000000c8" + ], + "scope": "Subtree", + "mappings": [ + { + "source": "c6060906-0000-4000-8000-000000001f40", + "target": "c6060906-0000-4000-8000-000000000032", + "target_kind": null + }, + { + "source": "c6060906-0000-4000-8000-00000000001e", + "target": "c6060906-0000-4000-8000-00000000000a", + "target_kind": null + }, + { + "source": "c6060906-0000-4000-8000-00000000001f", + "target": "c6060906-0000-4000-8000-00000000000b", + "target_kind": null + } + ], + "fields": [ + "Name", + "Description" + ], + "resolutions": [ + { + "source": "c6060906-0000-4000-8000-000000001f40", + "field": "Name", + "decision": "KeepTarget" + }, + { + "source": "c6060906-0000-4000-8000-00000000001e", + "field": "Name", + "decision": "KeepTarget" + }, + { + "source": "c6060906-0000-4000-8000-00000000001f", + "field": "Name", + "decision": "KeepTarget" + } + ], + "destination_diagram": null, + "communication_endpoints": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia new file mode 100644 index 00000000..6406e397 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-behavior.after-template/traceability.arcadia @@ -0,0 +1,91 @@ + +trace trace_oa_cm_200_cm_200 "Reverse rescue request ↔ Reverse rescue request" id "c6060906-0000-4000-8000-000000001f47" { + baseline { + oa { + description ""; + name "Reverse rescue request"; + }; + sa { + description ""; + name "Reverse rescue request"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_200; + sa @cm_200; +} + + +trace trace_oa_cm_201_cm_201 "Team radio lifeline ↔ Team radio lifeline" id "c6060906-0000-4000-8000-000000001f48" { + baseline { + oa { + description ""; + name "Team radio lifeline"; + }; + sa { + description ""; + name "Team radio lifeline"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_201; + sa @cm_201; +} + + +trace trace_oa_cm_202_cm_202 "Control radio lifeline ↔ Control radio lifeline" id "c6060906-0000-4000-8000-000000001f49" { + baseline { + oa { + description ""; + name "Control radio lifeline"; + }; + sa { + description ""; + name "Control radio lifeline"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_202; + sa @cm_202; +} + + +trace trace_oa_cm_203_cm_203 "Request rescue report ↔ Request rescue report" id "c6060906-0000-4000-8000-000000001f4a" { + baseline { + oa { + description ""; + name "Request rescue report"; + }; + sa { + description ""; + name "Request rescue report"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_203; + sa @cm_203; +} + + +trace trace_oa_cm_204_cm_204 "Reply with rescue report ↔ Reply with rescue report" id "c6060906-0000-4000-8000-000000001f4b" { + baseline { + oa { + description ""; + name "Reply with rescue report"; + }; + sa { + description ""; + name "Reply with rescue report"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_204; + sa @cm_204; +} + + diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia new file mode 100644 index 00000000..6f150af8 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/entry.arcadia @@ -0,0 +1,396 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + sa_scenario cm_200 "Reverse rescue request" id "c6060906-0000-4000-8000-0000000000c8" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@cm_203, @cm_204]; + viewpoint BoundaryInterface; + } + + sa_lifeline cm_201 "Team radio lifeline" id "c6060906-0000-4000-8000-0000000000c9" { + description ""; + parent @cm_200; + represented @cm_30; + } + + sa_lifeline cm_202 "Control radio lifeline" id "c6060906-0000-4000-8000-0000000000ca" { + description ""; + parent @cm_200; + represented @cm_31; + } + + sa_occurrence cm_203 "Request rescue report" id "c6060906-0000-4000-8000-0000000000cb" { + body { + Exchange { + exchange @sa_cm_50; + message_kind Call; + receiver @cm_201; + reply_to none; + sender @cm_202; + values []; + }; + }; + description ""; + parent @cm_200; + postcondition ""; + precondition ""; + } + + sa_occurrence cm_204 "Reply with rescue report" id "c6060906-0000-4000-8000-0000000000cc" { + body { + Exchange { + exchange @sa_cm_50; + message_kind Reply; + receiver @cm_202; + reply_to @cm_203; + sender @cm_201; + values []; + }; + }; + description ""; + parent @cm_200; + postcondition ""; + precondition ""; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + communication_mean cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000000032" { + description ""; + items []; + oriented false; + source @cm_10; + target @cm_11; + } + + boundary_exchange sa_cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000001f40" { + description ""; + items []; + oriented false; + source @cm_32; + target @cm_33; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline { + oa { + description ""; + name "Rescue team"; + }; + sa { + description ""; + name "Rescue radio"; + }; + }; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline { + oa { + description ""; + name "Control centre"; + }; + sa { + description ""; + name "Control radio"; + }; + }; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + + trace trace_exchange_exchange "Rescue radio channel ↔ Rescue radio channel" id "c6060906-0000-4000-8000-000000001f41" { + baseline { + oa { + description ""; + name "Rescue radio channel"; + }; + sa { + description ""; + name "Rescue radio channel"; + }; + }; + description ""; + kind Exchange; + oa @cm_50; + sa @sa_cm_50; + } + + + node view_control_radio_lifeline id "c6060906-0000-4000-8000-000000001f4c" { + collapsed false; + diagram @cm_diagram_1000; + element @oa_cm_202; + position { + x 60.0; + y 360.0; + }; + size { + height 94.0; + width 224.0; + }; + } + + + node view_reply_with_rescue_report id "c6060906-0000-4000-8000-000000001f4d" { + collapsed false; + diagram @cm_diagram_1000; + element @oa_cm_204; + position { + x 620.0; + y 360.0; + }; + size { + height 94.0; + width 224.0; + }; + } + + + node view_request_rescue_report id "c6060906-0000-4000-8000-000000001f4e" { + collapsed false; + diagram @cm_diagram_1000; + element @oa_cm_203; + position { + x 340.0; + y 360.0; + }; + size { + height 94.0; + width 224.0; + }; + } + + + node view_reverse_rescue_request id "c6060906-0000-4000-8000-000000001f4f" { + collapsed false; + diagram @cm_diagram_1000; + element @oa_cm_200; + position { + x 340.0; + y 210.0; + }; + size { + height 94.0; + width 224.0; + }; + } + + + node view_team_radio_lifeline id "c6060906-0000-4000-8000-000000001f50" { + collapsed false; + diagram @cm_diagram_1000; + element @oa_cm_201; + position { + x 620.0; + y 210.0; + }; + size { + height 94.0; + width 224.0; + }; + } + + + + include "oa/behavior/scenario_oa_cm_200.arcadia"; + include "traceability.arcadia"; +} diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json new file mode 100644 index 00000000..5346a895 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/expected.json @@ -0,0 +1,749 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000c8": { + "id": "c6060906-0000-4000-8000-0000000000c8", + "alias": "cm_200", + "name": "Reverse rescue request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Scenario": { + "viewpoint": "BoundaryInterface", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "c6060906-0000-4000-8000-0000000000cb", + "c6060906-0000-4000-8000-0000000000cc" + ] + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000c9": { + "id": "c6060906-0000-4000-8000-0000000000c9", + "alias": "cm_201", + "name": "Team radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000001e" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000ca": { + "id": "c6060906-0000-4000-8000-0000000000ca", + "alias": "cm_202", + "name": "Control radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000001f" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000cb": { + "id": "c6060906-0000-4000-8000-0000000000cb", + "alias": "cm_203", + "name": "Request rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000001f40", + "sender": "c6060906-0000-4000-8000-0000000000ca", + "receiver": "c6060906-0000-4000-8000-0000000000c9", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000cc": { + "id": "c6060906-0000-4000-8000-0000000000cc", + "alias": "cm_204", + "name": "Reply with rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000001f40", + "sender": "c6060906-0000-4000-8000-0000000000c9", + "receiver": "c6060906-0000-4000-8000-0000000000ca", + "message_kind": "Reply", + "reply_to": "c6060906-0000-4000-8000-0000000000cb", + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f42": { + "id": "c6060906-0000-4000-8000-000000001f42", + "alias": "oa_cm_200", + "name": "Reverse rescue request", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "Scenario": { + "viewpoint": "ParticipantInteraction", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "c6060906-0000-4000-8000-000000001f45", + "c6060906-0000-4000-8000-000000001f46" + ] + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f43": { + "id": "c6060906-0000-4000-8000-000000001f43", + "alias": "oa_cm_201", + "name": "Team radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000000a" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f44": { + "id": "c6060906-0000-4000-8000-000000001f44", + "alias": "oa_cm_202", + "name": "Control radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000000b" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f45": { + "id": "c6060906-0000-4000-8000-000000001f45", + "alias": "oa_cm_203", + "name": "Request rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000000032", + "sender": "c6060906-0000-4000-8000-000000001f44", + "receiver": "c6060906-0000-4000-8000-000000001f43", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-000000001f46": { + "id": "c6060906-0000-4000-8000-000000001f46", + "alias": "oa_cm_204", + "name": "Reply with rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-000000001f42", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000000032", + "sender": "c6060906-0000-4000-8000-000000001f43", + "receiver": "c6060906-0000-4000-8000-000000001f44", + "message_kind": "Reply", + "reply_to": "c6060906-0000-4000-8000-000000001f45", + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000000032": { + "id": "c6060906-0000-4000-8000-000000000032", + "alias": "cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "CommunicationMean": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000b", + "oriented": false, + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000001f40": { + "id": "c6060906-0000-4000-8000-000000001f40", + "alias": "sa_cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "BoundaryExchange": { + "source": "c6060906-0000-4000-8000-000000000020", + "target": "c6060906-0000-4000-8000-000000000021", + "oriented": false, + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000001f4c": { + "id": "c6060906-0000-4000-8000-000000001f4c", + "alias": "view_control_radio_lifeline", + "element": "c6060906-0000-4000-8000-000000001f44", + "position": { + "x": 60.0, + "y": 360.0 + }, + "size": { + "width": 224.0, + "height": 94.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000001f4d": { + "id": "c6060906-0000-4000-8000-000000001f4d", + "alias": "view_reply_with_rescue_report", + "element": "c6060906-0000-4000-8000-000000001f46", + "position": { + "x": 620.0, + "y": 360.0 + }, + "size": { + "width": 224.0, + "height": 94.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000001f4e": { + "id": "c6060906-0000-4000-8000-000000001f4e", + "alias": "view_request_rescue_report", + "element": "c6060906-0000-4000-8000-000000001f45", + "position": { + "x": 340.0, + "y": 360.0 + }, + "size": { + "width": 224.0, + "height": 94.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000001f4f": { + "id": "c6060906-0000-4000-8000-000000001f4f", + "alias": "view_reverse_rescue_request", + "element": "c6060906-0000-4000-8000-000000001f42", + "position": { + "x": 340.0, + "y": 210.0 + }, + "size": { + "width": 224.0, + "height": 94.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000001f50": { + "id": "c6060906-0000-4000-8000-000000001f50", + "alias": "view_team_radio_lifeline", + "element": "c6060906-0000-4000-8000-000000001f43", + "position": { + "x": 620.0, + "y": 210.0 + }, + "size": { + "width": 224.0, + "height": 94.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": { + "oa": { + "name": "Rescue team", + "description": "" + }, + "sa": { + "name": "Rescue radio", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": { + "oa": { + "name": "Control centre", + "description": "" + }, + "sa": { + "name": "Control radio", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f41": { + "id": "c6060906-0000-4000-8000-000000001f41", + "alias": "trace_exchange_exchange", + "name": "Rescue radio channel ↔ Rescue radio channel", + "description": "", + "oa": "c6060906-0000-4000-8000-000000000032", + "sa": "c6060906-0000-4000-8000-000000001f40", + "kind": "Exchange", + "baseline": { + "oa": { + "name": "Rescue radio channel", + "description": "" + }, + "sa": { + "name": "Rescue radio channel", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f47": { + "id": "c6060906-0000-4000-8000-000000001f47", + "alias": "trace_oa_cm_200_cm_200", + "name": "Reverse rescue request ↔ Reverse rescue request", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f42", + "sa": "c6060906-0000-4000-8000-0000000000c8", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Reverse rescue request", + "description": "" + }, + "sa": { + "name": "Reverse rescue request", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f48": { + "id": "c6060906-0000-4000-8000-000000001f48", + "alias": "trace_oa_cm_201_cm_201", + "name": "Team radio lifeline ↔ Team radio lifeline", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f43", + "sa": "c6060906-0000-4000-8000-0000000000c9", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Team radio lifeline", + "description": "" + }, + "sa": { + "name": "Team radio lifeline", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f49": { + "id": "c6060906-0000-4000-8000-000000001f49", + "alias": "trace_oa_cm_202_cm_202", + "name": "Control radio lifeline ↔ Control radio lifeline", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f44", + "sa": "c6060906-0000-4000-8000-0000000000ca", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Control radio lifeline", + "description": "" + }, + "sa": { + "name": "Control radio lifeline", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f4a": { + "id": "c6060906-0000-4000-8000-000000001f4a", + "alias": "trace_oa_cm_203_cm_203", + "name": "Request rescue report ↔ Request rescue report", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f45", + "sa": "c6060906-0000-4000-8000-0000000000cb", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Request rescue report", + "description": "" + }, + "sa": { + "name": "Request rescue report", + "description": "" + } + } + }, + "c6060906-0000-4000-8000-000000001f4b": { + "id": "c6060906-0000-4000-8000-000000001f4b", + "alias": "trace_oa_cm_204_cm_204", + "name": "Reply with rescue report ↔ Reply with rescue report", + "description": "", + "oa": "c6060906-0000-4000-8000-000000001f46", + "sa": "c6060906-0000-4000-8000-0000000000cc", + "kind": "Scenario", + "baseline": { + "oa": { + "name": "Reply with rescue report", + "description": "" + }, + "sa": { + "name": "Reply with rescue report", + "description": "" + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia new file mode 100644 index 00000000..e71448e3 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia @@ -0,0 +1,63 @@ + +oa_scenario oa_cm_200 "Reverse rescue request" id "c6060906-0000-4000-8000-000000001f42" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@oa_cm_203, @oa_cm_204]; + viewpoint ParticipantInteraction; + + oa_lifeline oa_cm_201 "Team radio lifeline" id "c6060906-0000-4000-8000-000000001f43" { + description ""; + + represented @cm_10; + } + + + oa_lifeline oa_cm_202 "Control radio lifeline" id "c6060906-0000-4000-8000-000000001f44" { + description ""; + + represented @cm_11; + } + + + oa_occurrence oa_cm_203 "Request rescue report" id "c6060906-0000-4000-8000-000000001f45" { + body { + Exchange { + exchange @cm_50; + message_kind Call; + receiver @oa_cm_201; + reply_to none; + sender @oa_cm_202; + values []; + }; + }; + description ""; + + postcondition ""; + precondition ""; + } + + + oa_occurrence oa_cm_204 "Reply with rescue report" id "c6060906-0000-4000-8000-000000001f46" { + body { + Exchange { + exchange @cm_50; + message_kind Reply; + receiver @oa_cm_202; + reply_to @oa_cm_203; + sender @oa_cm_201; + values []; + }; + }; + description ""; + + postcondition ""; + precondition ""; + } + + +} + + diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia new file mode 100644 index 00000000..6406e397 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.after-template/traceability.arcadia @@ -0,0 +1,91 @@ + +trace trace_oa_cm_200_cm_200 "Reverse rescue request ↔ Reverse rescue request" id "c6060906-0000-4000-8000-000000001f47" { + baseline { + oa { + description ""; + name "Reverse rescue request"; + }; + sa { + description ""; + name "Reverse rescue request"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_200; + sa @cm_200; +} + + +trace trace_oa_cm_201_cm_201 "Team radio lifeline ↔ Team radio lifeline" id "c6060906-0000-4000-8000-000000001f48" { + baseline { + oa { + description ""; + name "Team radio lifeline"; + }; + sa { + description ""; + name "Team radio lifeline"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_201; + sa @cm_201; +} + + +trace trace_oa_cm_202_cm_202 "Control radio lifeline ↔ Control radio lifeline" id "c6060906-0000-4000-8000-000000001f49" { + baseline { + oa { + description ""; + name "Control radio lifeline"; + }; + sa { + description ""; + name "Control radio lifeline"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_202; + sa @cm_202; +} + + +trace trace_oa_cm_203_cm_203 "Request rescue report ↔ Request rescue report" id "c6060906-0000-4000-8000-000000001f4a" { + baseline { + oa { + description ""; + name "Request rescue report"; + }; + sa { + description ""; + name "Request rescue report"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_203; + sa @cm_203; +} + + +trace trace_oa_cm_204_cm_204 "Reply with rescue report ↔ Reply with rescue report" id "c6060906-0000-4000-8000-000000001f4b" { + baseline { + oa { + description ""; + name "Reply with rescue report"; + }; + sa { + description ""; + name "Reply with rescue report"; + }; + }; + description ""; + kind Scenario; + oa @oa_cm_204; + sa @cm_204; +} + + diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia new file mode 100644 index 00000000..d3f3046c --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia @@ -0,0 +1,299 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + sa_scenario cm_200 "Reverse rescue request" id "c6060906-0000-4000-8000-0000000000c8" { + context none; + description ""; + parent none; + postcondition ""; + precondition ""; + steps [@cm_203, @cm_204]; + viewpoint BoundaryInterface; + } + + sa_lifeline cm_201 "Team radio lifeline" id "c6060906-0000-4000-8000-0000000000c9" { + description ""; + parent @cm_200; + represented @cm_30; + } + + sa_lifeline cm_202 "Control radio lifeline" id "c6060906-0000-4000-8000-0000000000ca" { + description ""; + parent @cm_200; + represented @cm_31; + } + + sa_occurrence cm_203 "Request rescue report" id "c6060906-0000-4000-8000-0000000000cb" { + body { + Exchange { + exchange @sa_cm_50; + message_kind Call; + receiver @cm_201; + reply_to none; + sender @cm_202; + values []; + }; + }; + description ""; + parent @cm_200; + postcondition ""; + precondition ""; + } + + sa_occurrence cm_204 "Reply with rescue report" id "c6060906-0000-4000-8000-0000000000cc" { + body { + Exchange { + exchange @sa_cm_50; + message_kind Reply; + receiver @cm_202; + reply_to @cm_203; + sender @cm_201; + values []; + }; + }; + description ""; + parent @cm_200; + postcondition ""; + precondition ""; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + communication_mean cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000000032" { + description ""; + items []; + oriented false; + source @cm_10; + target @cm_11; + } + + boundary_exchange sa_cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000001f40" { + description ""; + items []; + oriented false; + source @cm_32; + target @cm_33; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline none; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline none; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + + trace trace_exchange_exchange "Rescue radio channel ↔ Rescue radio channel" id "c6060906-0000-4000-8000-000000001f41" { + baseline { + oa { + description ""; + name "Rescue radio channel"; + }; + sa { + description ""; + name "Rescue radio channel"; + }; + }; + description ""; + kind Exchange; + oa @cm_50; + sa @sa_cm_50; + } + +} diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json new file mode 100644 index 00000000..298cf51b --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.json @@ -0,0 +1,457 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000c8": { + "id": "c6060906-0000-4000-8000-0000000000c8", + "alias": "cm_200", + "name": "Reverse rescue request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "Scenario": { + "viewpoint": "BoundaryInterface", + "context": null, + "precondition": "", + "postcondition": "", + "steps": [ + "c6060906-0000-4000-8000-0000000000cb", + "c6060906-0000-4000-8000-0000000000cc" + ] + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000c9": { + "id": "c6060906-0000-4000-8000-0000000000c9", + "alias": "cm_201", + "name": "Team radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000001e" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000ca": { + "id": "c6060906-0000-4000-8000-0000000000ca", + "alias": "cm_202", + "name": "Control radio lifeline", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Lifeline": { + "represented": "c6060906-0000-4000-8000-00000000001f" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000cb": { + "id": "c6060906-0000-4000-8000-0000000000cb", + "alias": "cm_203", + "name": "Request rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000001f40", + "sender": "c6060906-0000-4000-8000-0000000000ca", + "receiver": "c6060906-0000-4000-8000-0000000000c9", + "message_kind": "Call", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "c6060906-0000-4000-8000-0000000000cc": { + "id": "c6060906-0000-4000-8000-0000000000cc", + "alias": "cm_204", + "name": "Reply with rescue report", + "description": "", + "parent": "c6060906-0000-4000-8000-0000000000c8", + "body": { + "Sa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "c6060906-0000-4000-8000-000000001f40", + "sender": "c6060906-0000-4000-8000-0000000000c9", + "receiver": "c6060906-0000-4000-8000-0000000000ca", + "message_kind": "Reply", + "reply_to": "c6060906-0000-4000-8000-0000000000cb", + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000000032": { + "id": "c6060906-0000-4000-8000-000000000032", + "alias": "cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "CommunicationMean": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000b", + "oriented": false, + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000001f40": { + "id": "c6060906-0000-4000-8000-000000001f40", + "alias": "sa_cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "BoundaryExchange": { + "source": "c6060906-0000-4000-8000-000000000020", + "target": "c6060906-0000-4000-8000-000000000021", + "oriented": false, + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000001f41": { + "id": "c6060906-0000-4000-8000-000000001f41", + "alias": "trace_exchange_exchange", + "name": "Rescue radio channel ↔ Rescue radio channel", + "description": "", + "oa": "c6060906-0000-4000-8000-000000000032", + "sa": "c6060906-0000-4000-8000-000000001f40", + "kind": "Exchange", + "baseline": { + "oa": { + "name": "Rescue radio channel", + "description": "" + }, + "sa": { + "name": "Rescue radio channel", + "description": "" + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json new file mode 100644 index 00000000..99740913 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-reverse-scenario.request.json @@ -0,0 +1,47 @@ +{ + "direction": "SaToOa", + "sources": [ + "c6060906-0000-4000-8000-0000000000c8" + ], + "scope": "Subtree", + "mappings": [ + { + "source": "c6060906-0000-4000-8000-000000001f40", + "target": "c6060906-0000-4000-8000-000000000032", + "target_kind": null + }, + { + "source": "c6060906-0000-4000-8000-00000000001e", + "target": "c6060906-0000-4000-8000-00000000000a", + "target_kind": null + }, + { + "source": "c6060906-0000-4000-8000-00000000001f", + "target": "c6060906-0000-4000-8000-00000000000b", + "target_kind": null + } + ], + "fields": [ + "Name", + "Description" + ], + "resolutions": [ + { + "source": "c6060906-0000-4000-8000-000000001f40", + "field": "Name", + "decision": "KeepTarget" + }, + { + "source": "c6060906-0000-4000-8000-00000000001e", + "field": "Name", + "decision": "KeepTarget" + }, + { + "source": "c6060906-0000-4000-8000-00000000001f", + "field": "Name", + "decision": "KeepTarget" + } + ], + "destination_diagram": null, + "communication_endpoints": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia b/harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia new file mode 100644 index 00000000..5f481884 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-transition-ready.arcadia @@ -0,0 +1,218 @@ +arcadia 6; +project communication_native "Rescue communication engineering" id "c6060906-0000-4000-8000-000000000001" { + description ""; + + oa_entity cm_10 "Rescue team" id "c6060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_actor cm_11 "Control centre" id "c6060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity cm_12 "Report rescue status" id "c6060906-0000-4000-8000-00000000000c" { + description ""; + parent none; + } + + oa_activity cm_13 "Assess rescue status" id "c6060906-0000-4000-8000-00000000000d" { + description ""; + parent none; + } + + sa_system cm_30 "Rescue radio" id "c6060906-0000-4000-8000-00000000001e" { + description ""; + parent none; + } + + sa_actor cm_31 "Control radio" id "c6060906-0000-4000-8000-00000000001f" { + description ""; + parent none; + } + + sa_port cm_32 "Existing team socket" id "c6060906-0000-4000-8000-000000000020" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_30; + } + + sa_port cm_33 "Existing control socket" id "c6060906-0000-4000-8000-000000000021" { + contract { + Component { + kind Standard; + orientation Unset; + }; + }; + description ""; + legacy none; + parent @cm_31; + } + + allocation cm_20 "Team performs reporting" id "c6060906-0000-4000-8000-000000000014" { + description ""; + source @cm_10; + target @cm_12; + } + + allocation cm_21 "Control performs assessment" id "c6060906-0000-4000-8000-000000000015" { + description ""; + source @cm_11; + target @cm_13; + } + + interaction cm_22 "Rescue status" id "c6060906-0000-4000-8000-000000000016" { + description ""; + items []; + source @cm_12; + target @cm_13; + } + + communication_mean cm_50 "Rescue radio channel" id "c6060906-0000-4000-8000-000000000032" { + description ""; + items []; + oriented false; + source @cm_10; + target @cm_11; + } + + diagram cm_diagram_1000 "Draw operational communication" id "c6060906-0000-4000-8000-0000000003e8" { + description ""; + layer Oa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1000 id "c6060906-0000-4000-8000-0000000003f2" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_10; + position { + x 80.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1001 id "c6060906-0000-4000-8000-0000000003f3" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_11; + position { + x 550.0; + y 70.0; + }; + size { + height 160.0; + width 250.0; + }; + } + + node cm_view_1002 id "c6060906-0000-4000-8000-0000000003f4" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_12; + position { + x 80.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + node cm_view_1003 id "c6060906-0000-4000-8000-0000000003f5" { + collapsed false; + diagram @cm_diagram_1000; + element @cm_13; + position { + x 550.0; + y 330.0; + }; + size { + height 110.0; + width 250.0; + }; + } + + diagram cm_diagram_1100 "Explicit system endpoints" id "c6060906-0000-4000-8000-00000000044c" { + description ""; + layer Sa; + projection Overview; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node cm_view_1100 id "c6060906-0000-4000-8000-000000000456" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_30; + position { + x 80.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + node cm_view_1101 id "c6060906-0000-4000-8000-000000000457" { + collapsed false; + diagram @cm_diagram_1100; + element @cm_31; + position { + x 550.0; + y 140.0; + }; + size { + height 170.0; + width 250.0; + }; + } + + trace cm_40 "Team radio correspondence" id "c6060906-0000-4000-8000-000000000028" { + baseline none; + description ""; + kind Participant; + oa @cm_10; + sa @cm_30; + } + + trace cm_41 "Control radio correspondence" id "c6060906-0000-4000-8000-000000000029" { + baseline none; + description ""; + kind Participant; + oa @cm_11; + sa @cm_31; + } + +} diff --git a/harness/fixtures/release1-native/communication-means/cm-transition-ready.json b/harness/fixtures/release1-native/communication-means/cm-transition-ready.json new file mode 100644 index 00000000..ef28735d --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/cm-transition-ready.json @@ -0,0 +1,315 @@ +{ + "schema_version": 6, + "id": "c6060906-0000-4000-8000-000000000001", + "alias": "communication_native", + "name": "Rescue communication engineering", + "description": "", + "elements": { + "c6060906-0000-4000-8000-00000000000a": { + "id": "c6060906-0000-4000-8000-00000000000a", + "alias": "cm_10", + "name": "Rescue team", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "c6060906-0000-4000-8000-00000000000b": { + "id": "c6060906-0000-4000-8000-00000000000b", + "alias": "cm_11", + "name": "Control centre", + "description": "", + "parent": null, + "body": { + "Oa": "Actor" + } + }, + "c6060906-0000-4000-8000-00000000000c": { + "id": "c6060906-0000-4000-8000-00000000000c", + "alias": "cm_12", + "name": "Report rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000000d": { + "id": "c6060906-0000-4000-8000-00000000000d", + "alias": "cm_13", + "name": "Assess rescue status", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c6060906-0000-4000-8000-00000000001e": { + "id": "c6060906-0000-4000-8000-00000000001e", + "alias": "cm_30", + "name": "Rescue radio", + "description": "", + "parent": null, + "body": { + "Sa": "System" + } + }, + "c6060906-0000-4000-8000-00000000001f": { + "id": "c6060906-0000-4000-8000-00000000001f", + "alias": "cm_31", + "name": "Control radio", + "description": "", + "parent": null, + "body": { + "Sa": "Actor" + } + }, + "c6060906-0000-4000-8000-000000000020": { + "id": "c6060906-0000-4000-8000-000000000020", + "alias": "cm_32", + "name": "Existing team socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001e", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + }, + "c6060906-0000-4000-8000-000000000021": { + "id": "c6060906-0000-4000-8000-000000000021", + "alias": "cm_33", + "name": "Existing control socket", + "description": "", + "parent": "c6060906-0000-4000-8000-00000000001f", + "body": { + "Sa": { + "Shared": { + "Port": { + "contract": { + "Component": { + "kind": "Standard", + "orientation": "Unset" + } + }, + "legacy": null + } + } + } + } + } + }, + "relations": { + "c6060906-0000-4000-8000-000000000014": { + "id": "c6060906-0000-4000-8000-000000000014", + "alias": "cm_20", + "name": "Team performs reporting", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000c" + } + } + }, + "c6060906-0000-4000-8000-000000000015": { + "id": "c6060906-0000-4000-8000-000000000015", + "alias": "cm_21", + "name": "Control performs assessment", + "description": "", + "body": { + "Allocation": { + "source": "c6060906-0000-4000-8000-00000000000b", + "target": "c6060906-0000-4000-8000-00000000000d" + } + } + }, + "c6060906-0000-4000-8000-000000000016": { + "id": "c6060906-0000-4000-8000-000000000016", + "alias": "cm_22", + "name": "Rescue status", + "description": "", + "body": { + "Interaction": { + "source": "c6060906-0000-4000-8000-00000000000c", + "target": "c6060906-0000-4000-8000-00000000000d", + "items": [] + } + } + }, + "c6060906-0000-4000-8000-000000000032": { + "id": "c6060906-0000-4000-8000-000000000032", + "alias": "cm_50", + "name": "Rescue radio channel", + "description": "", + "body": { + "CommunicationMean": { + "source": "c6060906-0000-4000-8000-00000000000a", + "target": "c6060906-0000-4000-8000-00000000000b", + "oriented": false, + "items": [] + } + } + } + }, + "diagrams": { + "c6060906-0000-4000-8000-0000000003e8": { + "id": "c6060906-0000-4000-8000-0000000003e8", + "alias": "cm_diagram_1000", + "name": "Draw operational communication", + "description": "", + "layer": "Oa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-0000000003f2": { + "id": "c6060906-0000-4000-8000-0000000003f2", + "alias": "cm_view_1000", + "element": "c6060906-0000-4000-8000-00000000000a", + "position": { + "x": 80.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f3": { + "id": "c6060906-0000-4000-8000-0000000003f3", + "alias": "cm_view_1001", + "element": "c6060906-0000-4000-8000-00000000000b", + "position": { + "x": 550.0, + "y": 70.0 + }, + "size": { + "width": 250.0, + "height": 160.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f4": { + "id": "c6060906-0000-4000-8000-0000000003f4", + "alias": "cm_view_1002", + "element": "c6060906-0000-4000-8000-00000000000c", + "position": { + "x": 80.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-0000000003f5": { + "id": "c6060906-0000-4000-8000-0000000003f5", + "alias": "cm_view_1003", + "element": "c6060906-0000-4000-8000-00000000000d", + "position": { + "x": 550.0, + "y": 330.0 + }, + "size": { + "width": 250.0, + "height": 110.0 + }, + "collapsed": false + } + }, + "routes": [] + }, + "c6060906-0000-4000-8000-00000000044c": { + "id": "c6060906-0000-4000-8000-00000000044c", + "alias": "cm_diagram_1100", + "name": "Explicit system endpoints", + "description": "", + "layer": "Sa", + "projection": "Overview", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "c6060906-0000-4000-8000-000000000456": { + "id": "c6060906-0000-4000-8000-000000000456", + "alias": "cm_view_1100", + "element": "c6060906-0000-4000-8000-00000000001e", + "position": { + "x": 80.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + }, + "c6060906-0000-4000-8000-000000000457": { + "id": "c6060906-0000-4000-8000-000000000457", + "alias": "cm_view_1101", + "element": "c6060906-0000-4000-8000-00000000001f", + "position": { + "x": 550.0, + "y": 140.0 + }, + "size": { + "width": 250.0, + "height": 170.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": { + "c6060906-0000-4000-8000-000000000028": { + "id": "c6060906-0000-4000-8000-000000000028", + "alias": "cm_40", + "name": "Team radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000a", + "sa": "c6060906-0000-4000-8000-00000000001e", + "kind": "Participant", + "baseline": null + }, + "c6060906-0000-4000-8000-000000000029": { + "id": "c6060906-0000-4000-8000-000000000029", + "alias": "cm_41", + "name": "Control radio correspondence", + "description": "", + "oa": "c6060906-0000-4000-8000-00000000000b", + "sa": "c6060906-0000-4000-8000-00000000001f", + "kind": "Participant", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/communication-means/generate-communication-means.rs b/harness/fixtures/release1-native/communication-means/generate-communication-means.rs new file mode 100644 index 00000000..21ae9ca2 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/generate-communication-means.rs @@ -0,0 +1,874 @@ +//! Direct core fixture/oracle helper; no native input or accepted-file mutation. +use oa_core::{transitions::*, view_options::ViewOptions, *}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs, + path::Path, +}; + +fn id(n: u32) -> Id { + format!("c6060906-0000-4000-8000-{n:012x}").parse().unwrap() +} +fn element( + p: &mut Project, + n: u32, + kind: Kind, + layer: Layer, + name: &str, + parent: Option, +) -> Id { + let key = id(n); + p.elements.insert( + key, + Element { + id: key, + alias: format!("cm_{n}"), + name: name.into(), + description: String::new(), + parent, + body: if kind == Kind::Port { + ElementBody::shared_in( + layer, + SharedBody::Port { + contract: PortContract::Component { + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }, + legacy: None, + }, + ) + } else { + ElementBody::new(kind, layer).unwrap() + }, + }, + ); + key +} +fn relation(p: &mut Project, n: u32, kind: RelationKind, name: &str, source: Id, target: Id) -> Id { + let key = id(n); + p.relations.insert( + key, + Relation { + id: key, + alias: format!("cm_{n}"), + name: name.into(), + description: String::new(), + body: RelationBody::new(kind, source, target), + }, + ); + key +} +fn trace(p: &mut Project, n: u32, oa: Id, sa: Id, kind: TraceKind, name: &str) { + p.traces.insert( + id(n), + Trace { + id: id(n), + alias: format!("cm_{n}"), + name: name.into(), + description: String::new(), + oa, + sa, + kind, + baseline: None, + }, + ); +} +fn diagram(p: &mut Project, n: u32, layer: Layer, name: &str, rows: &[(Id, f32, f32, f32, f32)]) { + let nodes = rows + .iter() + .enumerate() + .map(|(i, &(element, x, y, width, height))| { + let key = id(n + 10 + i as u32); + ( + key, + NodePlacement { + id: key, + alias: format!("cm_view_{}", n + i as u32), + element, + position: Position { x, y }, + size: Size { width, height }, + collapsed: false, + port_anchors: vec![], + }, + ) + }) + .collect(); + p.diagrams.insert( + id(n), + Diagram { + id: id(n), + alias: format!("cm_diagram_{n}"), + name: name.into(), + description: String::new(), + layer, + projection: ProjectionKind::Overview, + view_options: ViewOptions::default(), + nodes, + routes: vec![], + }, + ); +} +fn port(p: &mut Project, n: u32, owner: Id, name: &str) -> Id { + let key = element(p, n, Kind::Port, Layer::Sa, name, Some(owner)); + if let Some(SharedBody::Port { contract, .. }) = p.elements.get_mut(&key).unwrap().shared_mut() + { + *contract = PortContract::Component { + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + }; + } else { + panic!("port"); + } + key +} +fn base() -> Project { + let mut p = Project::empty(); + p.id = id(1); + p.alias = "communication_native".into(); + p.name = "Rescue communication engineering".into(); + let a = element(&mut p, 10, Kind::Entity, Layer::Oa, "Rescue team", None); + let b = element(&mut p, 11, Kind::Actor, Layer::Oa, "Control centre", None); + let x = element( + &mut p, + 12, + Kind::Activity, + Layer::Oa, + "Report rescue status", + None, + ); + let y = element( + &mut p, + 13, + Kind::Activity, + Layer::Oa, + "Assess rescue status", + None, + ); + relation( + &mut p, + 20, + RelationKind::Allocation, + "Team performs reporting", + a, + x, + ); + relation( + &mut p, + 21, + RelationKind::Allocation, + "Control performs assessment", + b, + y, + ); + relation(&mut p, 22, RelationKind::Interaction, "Rescue status", x, y); + let sa = element(&mut p, 30, Kind::System, Layer::Sa, "Rescue radio", None); + let sb = element( + &mut p, + 31, + Kind::SystemActor, + Layer::Sa, + "Control radio", + None, + ); + port(&mut p, 32, sa, "Existing team socket"); + port(&mut p, 33, sb, "Existing control socket"); + trace( + &mut p, + 40, + a, + sa, + TraceKind::Participant, + "Team radio correspondence", + ); + trace( + &mut p, + 41, + b, + sb, + TraceKind::Participant, + "Control radio correspondence", + ); + diagram( + &mut p, + 1000, + Layer::Oa, + "Draw operational communication", + &[ + (a, 80., 70., 250., 160.), + (b, 550., 70., 250., 160.), + (x, 80., 330., 250., 110.), + (y, 550., 330., 250., 110.), + ], + ); + diagram( + &mut p, + 1100, + Layer::Sa, + "Explicit system endpoints", + &[(sa, 80., 140., 250., 170.), (sb, 550., 140., 250., 170.)], + ); + valid(&p); + p +} +fn valid(p: &Project) { + validate_candidate(p).unwrap(); + assert_eq!(language::parse(&language::format(p)).unwrap(), *p); + assert_eq!( + persistence::decode(&serde_json::to_vec(p).unwrap()).unwrap(), + *p + ); +} +fn forward(p: &Project, mean: Id, create: Option) -> TransitionRequest { + let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); + r.mappings = vec![ + TransitionMapping { + source: id(10), + target: Some(id(30)), + target_kind: None, + }, + TransitionMapping { + source: id(11), + target: Some(id(31)), + target_kind: None, + }, + ]; + r.resolutions = r + .mappings + .iter() + .map(|m| FieldResolution { + source: m.source, + field: TransitionField::Name, + decision: FieldDecision::KeepTarget, + }) + .collect(); + let target_port = if let Some(key) = create { + let name = format!("{} · target port", p.named(mean)); + CommunicationPortChoice::Create { + id: key, + alias: p.fresh_alias(&name, "port", key), + name, + description: String::new(), + kind: ComponentPortKind::Standard, + orientation: ComponentPortOrientation::Unset, + } + } else { + CommunicationPortChoice::Reuse { port: id(33) } + }; + r.communication_endpoints + .push(CommunicationEndpointDecision { + source: mean, + source_port: CommunicationPortChoice::Reuse { port: id(32) }, + target_port, + }); + r +} +fn apply(p: &Project, r: &TransitionRequest) -> Project { + let plan = plan_transition(p, r).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let mut q = apply_transition(p, &plan).unwrap(); + // Desktop Workflow::Transition::Apply adds presentation after the core + // transition. Predict its policy from the plan and prior diagram, never + // adopt placement coordinates/size/alias from the observed native result. + let diagram = r + .destination_diagram + .or_else(|| { + q.diagrams + .values() + .find(|d| d.layer == r.direction.target_layer()) + .map(|d| d.id) + }) + .expect("These fixtures provide an existing destination-layer diagram"); + for change in &plan.changes { + if q.elements.contains_key(&change.target) { + let count = q.diagrams[&diagram].nodes.len(); + q.place( + diagram, + change.target, + Position { + x: 60. + (count % 3) as f32 * 280., + y: 60. + (count / 3) as f32 * 150., + }, + ); + } + } + valid(&q); + q +} +// Separate policy oracle: the explicit scenario root opens in Behavior, so no +// architecture placement is requested. Keep apply() above for the legacy binary. +fn apply_reverse_behavior(p: &Project, r: &TransitionRequest) -> Project { + assert_eq!(r.sources, vec![id(200)]); + assert_eq!(p.elements[&id(200)].kind(), Kind::Scenario); + assert!(r.destination_diagram.is_none()); + let plan = plan_transition(p, r).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let q = apply_transition(p, &plan).unwrap(); + assert_eq!( + q.diagrams, p.diagrams, + "Behavior must preserve every saved view" + ); + reverse_assertions(p, &q); + valid(&q); + q +} + +fn generate_reverse_behavior(before: &Path, out: &Path) { + let disk = + source_files::load_project(before, source_project::ProjectLimits::default()).unwrap(); + let compiled = source_project::compile_bundle( + &disk.reachable_bundle(), + source_project::ProjectLimits::default(), + ) + .unwrap(); + let p = &compiled.decoded.project; + let request = reverse(p); + let q = deterministic(p, apply_reverse_behavior(p, &request)); + let updated = + source_project::update_bundle(&compiled, &q, source_project::ProjectLimits::default()) + .unwrap(); + fs::create_dir_all(out).unwrap(); + write_bundle(out, &updated.bundle); + fs::write( + out.join("expected.json"), + serde_json::to_vec_pretty(&q).unwrap(), + ) + .unwrap(); + fs::write( + out.join("request.json"), + serde_json::to_vec_pretty(&request).unwrap(), + ) + .unwrap(); + println!("PASS generate-reverse-behavior: core candidate, all saved views unchanged"); +} +// Only actual identity fields and references are remapped, never names/text/data. +fn remap(mut p: Project, mappings: &BTreeMap) -> Project { + for (&old, &new) in mappings { + if old == new { + continue; + } + assert!(!p.contains(new)); + references::remap_references(&mut p, old, new); + if let Some(mut e) = p.elements.remove(&old) { + e.id = new; + p.elements.insert(new, e); + } + if let Some(mut e) = p.relations.remove(&old) { + e.id = new; + p.relations.insert(new, e); + } + if let Some(mut e) = p.traces.remove(&old) { + e.id = new; + p.traces.insert(new, e); + } + if let Some(mut e) = p.artifact_links.remove(&old) { + e.id = new; + p.artifact_links.insert(new, e); + } + if let Some(mut e) = p.diagrams.remove(&old) { + e.id = new; + p.diagrams.insert(new, e); + } + for d in p.diagrams.values_mut() { + if let Some(mut e) = d.nodes.remove(&old) { + e.id = new; + d.nodes.insert(new, e); + } + } + } + valid(&p); + p +} +fn deterministic(base: &Project, q: Project) -> Project { + let mut fresh: Vec<_> = q + .identities() + .into_iter() + .filter(|(key, _)| !base.contains(*key)) + .collect(); + fresh.sort_by(|a, b| a.1.cmp(&b.1)); + let mut next = 8000; + let map = fresh + .into_iter() + .map(|(old, _)| { + while base.contains(id(next)) { + next += 1; + } + let new = id(next); + next += 1; + (old, new) + }) + .collect(); + remap(q, &map) +} +fn emit(dir: &Path, name: &str, p: &Project) { + valid(p); + fs::write(dir.join(format!("{name}.arcadia")), language::format(p)).unwrap(); + fs::write( + dir.join(format!("{name}.json")), + serde_json::to_vec_pretty(p).unwrap(), + ) + .unwrap(); +} +fn emit_after(dir: &Path, name: &str, base: &Project, after: &Project) { + let entry = "entry.arcadia".to_string(); + let bundle = source_project::SourceBundle { + entry: entry.clone(), + files: BTreeMap::from([(entry.clone(), language::format(base))]), + }; + let compiled = + source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap(); + let updated = + source_project::update_bundle(&compiled, after, source_project::ProjectLimits::default()) + .unwrap(); + let output = dir.join(name); + write_bundle(&output, &updated.bundle); + fs::write( + output.join("expected.json"), + serde_json::to_vec_pretty(after).unwrap(), + ) + .unwrap(); +} +fn write_bundle(dir: &Path, bundle: &source_project::SourceBundle) { + fs::create_dir_all(dir).unwrap(); + for (name, text) in &bundle.files { + let path = dir.join(name); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, text).unwrap(); + } +} +fn scenario(p: &mut Project, boundary: Id) -> Id { + let s = element( + p, + 200, + Kind::Scenario, + Layer::Sa, + "Reverse rescue request", + None, + ); + if let Some(SharedBody::Scenario { viewpoint, .. }) = + p.elements.get_mut(&s).unwrap().shared_mut() + { + *viewpoint = ScenarioViewpoint::BoundaryInterface; + } + let a = element( + p, + 201, + Kind::Lifeline, + Layer::Sa, + "Team radio lifeline", + Some(s), + ); + let b = element( + p, + 202, + Kind::Lifeline, + Layer::Sa, + "Control radio lifeline", + Some(s), + ); + for (line, owner) in [(a, id(30)), (b, id(31))] { + *p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { + represented: Some(owner), + }; + } + for (n, name, sender, receiver, kind, reply_to) in [ + (203, "Request rescue report", b, a, MessageKind::Call, None), + ( + 204, + "Reply with rescue report", + a, + b, + MessageKind::Reply, + Some(id(203)), + ), + ] { + let e = element(p, n, Kind::Occurrence, Layer::Sa, name, Some(s)); + *p.elements.get_mut(&e).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { + body: OccurrenceBody::Exchange { + exchange: Some(boundary), + sender: Some(sender), + receiver: Some(receiver), + message_kind: kind, + reply_to, + values: vec![], + }, + precondition: String::new(), + postcondition: String::new(), + }; + } + *p.elements.get_mut(&s).unwrap().steps_mut().unwrap() = vec![id(203), id(204)]; + valid(p); + s +} +fn reverse(p: &Project) -> TransitionRequest { + let mean = id(50); + let boundary = p.traces.values().find(|t| t.oa == mean).unwrap().sa; + let mut r = TransitionRequest::new(TransitionDirection::SaToOa, vec![id(200)]); + r.scope = DependencyScope::Subtree; + r.mappings = [(boundary, mean), (id(30), id(10)), (id(31), id(11))] + .into_iter() + .map(|(source, target)| TransitionMapping { + source, + target: Some(target), + target_kind: None, + }) + .collect(); + r.resolutions = r + .mappings + .iter() + .map(|m| FieldResolution { + source: m.source, + field: TransitionField::Name, + decision: FieldDecision::KeepTarget, + }) + .collect(); + r +} +fn reverse_assertions(p: &Project, q: &Project) { + use validation::MessageEndpointResolution as R; + let mapped = |source| q.traces.values().find(|t| t.sa == source).unwrap().oa; + let body = |id| match q.elements[&id].shared().unwrap() { + SharedBody::Occurrence { body, .. } => body, + _ => panic!(), + }; + let call = mapped(id(203)); + let reply = mapped(id(204)); + assert_eq!( + validation::resolve_message_endpoints(q, body(call)), + R::Resolved { + source: id(11), + target: id(10) + } + ); + assert_eq!( + validation::resolve_message_endpoints(q, body(reply)), + R::Resolved { + source: id(10), + target: id(11) + } + ); + assert!(matches!(body(reply),OccurrenceBody::Exchange{reply_to:Some(id),..} if *id==call)); + for key in [id(200), id(201), id(202), id(203), id(204)] { + assert_eq!(p.elements[&key], q.elements[&key]); + } + assert_eq!( + q.elements + .values() + .filter(|e| e.layer() == Layer::Oa && e.kind() == Kind::Port) + .count(), + 0 + ); +} +fn generate(dir: &Path) { + fs::create_dir_all(dir).unwrap(); + let p = base(); + emit(dir, "cm-draw", &p); + let mut ready = p.clone(); + relation( + &mut ready, + 50, + RelationKind::CommunicationMean, + "Rescue radio channel", + id(10), + id(11), + ); + valid(&ready); + emit(dir, "cm-transition-ready", &ready); + for (label, created) in [("reuse", None), ("create", Some(id(60)))] { + let r = forward(&ready, id(50), created); + let q = apply(&ready, &r); + let q = deterministic(&ready, q); + emit_after(dir, &format!("cm-{label}.after-template"), &ready, &q); + fs::write( + dir.join(format!("cm-{label}.request.json")), + serde_json::to_vec_pretty(&r).unwrap(), + ) + .unwrap(); + } + let mut p = deterministic(&ready, apply(&ready, &forward(&ready, id(50), None))); + let boundary = p.traces.values().find(|t| t.oa == id(50)).unwrap().sa; + scenario(&mut p, boundary); + emit(dir, "cm-reverse-scenario", &p); + let r = reverse(&p); + let q = apply(&p, &r); + reverse_assertions(&p, &q); + let q = deterministic(&p, q); + emit_after(dir, "cm-reverse-scenario.after-template", &p, &q); + fs::write( + dir.join("cm-reverse-scenario.request.json"), + serde_json::to_vec_pretty(&r).unwrap(), + ) + .unwrap(); + let manifest = serde_json::json!({"schema":6,"native_status":"pending","project":id(1),"oa_participants":[id(10),id(11)],"oa_activities":[id(12),id(13)],"interaction":id(22),"sa_participants":[id(30),id(31)],"existing_ports":[id(32),id(33)],"participant_traces":[id(40),id(41)],"draw_diagram":id(1000),"system_diagram":id(1100),"template_mean":id(50),"reverse_scenario":id(200),"reverse_call":id(203),"correlated_reply":id(204),"templates":"Generated target/trace UUIDs are reference examples, not native exact expectations. Use check with native before/after sources."}); + fs::write( + dir.join("manifest.json"), + serde_json::to_vec_pretty(&manifest).unwrap(), + ) + .unwrap(); +} +fn check_draw( + before: &source_project::CompiledSourceProject, + actual: &source_project::CompiledSourceProject, + out: &Path, +) { + let p = &before.decoded.project; + assert!( + !p.relations + .values() + .any(|r| r.kind() == RelationKind::CommunicationMean) + ); + let means: Vec<_> = actual + .decoded + .project + .relations + .values() + .filter(|r| r.kind() == RelationKind::CommunicationMean) + .collect(); + assert_eq!( + means.len(), + 1, + "Drawing creates exactly one communication means" + ); + let identity = means[0].id; + assert!(!p.contains(identity)); + let name = format!( + "Communication means: {} — {}", + p.named(id(10)), + p.named(id(11)) + ); + let drawn = Relation { + id: identity, + alias: p.fresh_alias(&name, "relation", identity), + name, + description: String::new(), + body: RelationBody::CommunicationMean { + source: id(10), + target: id(11), + oriented: false, + items: vec![], + }, + }; + let create = Command::CreateRelation(drawn.clone()); + let after_draw = execute_batch(p, std::slice::from_ref(&create)).unwrap(); + let draw_source = source_project::update_bundle( + before, + &after_draw, + source_project::ProjectLimits::default(), + ) + .unwrap(); + let mut renamed = drawn; + renamed.name = "Rescue radio channel".into(); + let rename = Command::EditRelation(identity, renamed); + let expected = execute_batch(&after_draw, std::slice::from_ref(&rename)).unwrap(); + let expected_source = source_project::update_bundle( + &draw_source, + &expected, + source_project::ProjectLimits::default(), + ) + .unwrap(); + assert_eq!( + expected, actual.decoded.project, + "Draw+rename full model differs; only actual new relation UUID was supplied" + ); + let mut actual_files = actual.bundle.files.clone(); + let entry = actual_files.remove(&actual.bundle.entry).unwrap(); + assert!( + actual_files + .insert(expected_source.bundle.entry.clone(), entry) + .is_none() + ); + assert_eq!( + expected_source.bundle.files, actual_files, + "Draw+rename complete source bytes differ (entry filename only aligned)" + ); + write_bundle(out, &expected_source.bundle); + fs::write( + out.join("expected-draw.json"), + serde_json::to_vec_pretty(&expected).unwrap(), + ) + .unwrap(); + fs::write( + out.join("draw-rename-commands.json"), + serde_json::to_vec_pretty(&[create, rename]).unwrap(), + ) + .unwrap(); + println!( + "PASS draw: typed CreateRelation then EditRelation, exact full model/source, only new relation UUID {identity}" + ); +} +fn check(mode: &str, before: &Path, after: &Path, out: &Path) { + let before_disk = + source_files::load_project(before, source_project::ProjectLimits::default()).unwrap(); + let actual_disk = + source_files::load_project(after, source_project::ProjectLimits::default()).unwrap(); + let compiled = source_project::compile_bundle( + &before_disk.reachable_bundle(), + source_project::ProjectLimits::default(), + ) + .unwrap(); + let actual_compiled = source_project::compile_bundle( + &actual_disk.reachable_bundle(), + source_project::ProjectLimits::default(), + ) + .unwrap(); + let p = compiled.decoded.project.clone(); + let actual = actual_compiled.decoded.project.clone(); + valid(&p); + valid(&actual); + if mode == "draw" { + return check_draw(&compiled, &actual_compiled, out); + } + let r = if matches!(mode, "reverse" | "reverse-behavior") { + reverse(&p) + } else { + let means: Vec<_> = p + .relations + .values() + .filter(|r| r.kind() == RelationKind::CommunicationMean) + .collect(); + assert_eq!(means.len(), 1); + let mean = means[0].id; + let created = if mode == "create" { + let trace = actual.traces.values().find(|t| t.oa == mean).unwrap(); + let key = actual.relations[&trace.sa].target(); + assert!(!p.contains(key)); + Some(key) + } else { + assert_eq!(mode, "reuse"); + None + }; + forward(&p, mean, created) + }; + let mut expected = if mode == "reverse-behavior" { + apply_reverse_behavior(&p, &r) + } else { + apply(&p, &r) + }; + let mut map = BTreeMap::new(); + let mut actual_used = BTreeSet::new(); + for t in expected + .traces + .values() + .filter(|t| !p.traces.contains_key(&t.id)) + { + let source = if r.direction == TransitionDirection::OaToSa { + t.oa + } else { + t.sa + }; + let counterparts: Vec<_> = actual + .traces + .values() + .filter(|a| { + if r.direction == TransitionDirection::OaToSa { + a.oa == source + } else { + a.sa == source + } + }) + .collect(); + assert_eq!( + counterparts.len(), + 1, + "each explicit source has one counterpart" + ); + let a = counterparts[0]; + assert!(!p.contains(a.id)); + assert!(actual_used.insert(a.id)); + map.insert(t.id, a.id); + let (old, new) = if r.direction == TransitionDirection::OaToSa { + (t.sa, a.sa) + } else { + (t.oa, a.oa) + }; + if !p.contains(old) { + assert!(!p.contains(new)); + assert!(actual_used.insert(new)); + map.insert(old, new); + } + } + // Match fresh presentation identities by exact destination diagram + mapped + // element. Every other placement property remains independently expected. + let mut placement_count = 0; + for (diagram, view) in &expected.diagrams { + for node in view.nodes.values().filter(|node| !p.contains(node.id)) { + let mapped_element = map.get(&node.element).copied().unwrap_or(node.element); + let actual_nodes: Vec<_> = actual + .diagrams + .get(diagram) + .expect("Expected destination diagram remains") + .nodes + .values() + .filter(|n| n.element == mapped_element) + .collect(); + assert_eq!( + actual_nodes.len(), + 1, + "Each generated placement must identify its exact diagram and mapped element" + ); + let actual_id = actual_nodes[0].id; + assert!(!p.contains(actual_id)); + assert!(actual_used.insert(actual_id)); + map.insert(node.id, actual_id); + placement_count += 1; + } + } + expected = remap(expected, &map); + if matches!(mode, "reverse" | "reverse-behavior") { + reverse_assertions(&p, &expected); + } + assert_eq!( + expected, actual, + "full model oracle differs after UUID-only normalization" + ); + let updated = source_project::update_bundle( + &compiled, + &expected, + source_project::ProjectLimits::default(), + ) + .unwrap(); + let mut actual_files = actual_compiled.bundle.files.clone(); + let actual_entry = actual_files.remove(&actual_compiled.bundle.entry).unwrap(); + assert!( + actual_files + .insert(updated.bundle.entry.clone(), actual_entry) + .is_none() + ); + assert_eq!( + updated.bundle.files, actual_files, + "complete exact source bundle differs (only entry filename normalized for Save As)" + ); + fs::create_dir_all(out).unwrap(); + write_bundle(out, &updated.bundle); + fs::write( + out.join("expected-native.json"), + serde_json::to_vec_pretty(&expected).unwrap(), + ) + .unwrap(); + fs::write( + out.join("native-request.json"), + serde_json::to_vec_pretty(&r).unwrap(), + ) + .unwrap(); + println!( + "PASS {mode}: complete model and exact source; {} generated UUID substitutions only ({placement_count} placement identities)", + map.len() + ); +} +fn main() { + let args: Vec<_> = std::env::args().collect(); + match args.get(1).map(String::as_str) { + Some("generate") if args.len() == 3 => generate(Path::new(&args[2])), + Some("generate-reverse-behavior") if args.len() == 4 => { + generate_reverse_behavior(Path::new(&args[2]), Path::new(&args[3])) + } + Some("check") if args.len() == 6 => check( + &args[2], + Path::new(&args[3]), + Path::new(&args[4]), + Path::new(&args[5]), + ), + _ => panic!( + "generate DIRECTORY | generate-reverse-behavior BEFORE.arcadia OUTPUT_DIRECTORY | check draw|reuse|create|reverse|reverse-behavior BEFORE.arcadia AFTER.arcadia OUTPUT_DIRECTORY" + ), + } +} diff --git a/harness/fixtures/release1-native/communication-means/hashes.json b/harness/fixtures/release1-native/communication-means/hashes.json new file mode 100644 index 00000000..e55250ec --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/hashes.json @@ -0,0 +1,32 @@ +{ + "algorithm": "SHA256", + "files": { + "cm-create.after-template/entry.arcadia": "a6976e7c21031e7cc3cb731e20c3075cf89cf85144dc8d3f52930f8521ea196f", + "cm-create.after-template/expected.json": "60c4419415c546f06c9972ac7bb0e530ada7b30a200b59e75ddda231a24f9a3d", + "cm-create.after-template/traceability.arcadia": "3c41459f2b0706a767988738bf8126f76540415bb5940d9c5db19cbe8c18182d", + "cm-create.request.json": "b16e673b29c96b5aed681389ff7669110abe552939e86dd0ec6011c9a45ed860", + "cm-draw.arcadia": "a511a4d817d9c5c80233bce83e84a7056b7dce7afd9fcc25696e333f8f4df055", + "cm-draw.json": "7140114926228b1c378c450535f7a70992c71a879a8d2270c49e8d5f04a2f484", + "cm-reuse.after-template/entry.arcadia": "910e6dfe609467047ce629ec9c626f4e927cada74bfbfa164414a8fe6783ea87", + "cm-reuse.after-template/expected.json": "567259e20c9f6278776e8fa99b8b2fdbbc9a883ad875c5d39dae1b53de3a2924", + "cm-reuse.after-template/traceability.arcadia": "997f72fe9258012df66e88344cf39c1a2cebf2327f604056b67f7f2a8ebcfbfa", + "cm-reuse.request.json": "cb3fe05859cfffec6f8846c19983dc462413bc0ea4f6e19204f09c259919c4b1", + "cm-reverse-behavior.after-template/cm-reverse-scenario.arcadia": "ec826b9b84616507490ac30e4d57100dd6338dba2f7071e879a8fb58820a85c5", + "cm-reverse-behavior.after-template/expected.json": "ef07e4d2d3fc36004ff01739838281581d9a703c747611eacc01bb9cc672489f", + "cm-reverse-behavior.after-template/oa/behavior/scenario_oa_cm_200.arcadia": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70", + "cm-reverse-behavior.after-template/request.json": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c", + "cm-reverse-behavior.after-template/traceability.arcadia": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858", + "cm-reverse-scenario.after-template/entry.arcadia": "86dd8a355f6c9dadfea56d06e5247c9b0f820819d7df17ef5690671c7c81324b", + "cm-reverse-scenario.after-template/expected.json": "980489ec2c483d4f9efada61358c1f29bafb2572fd5b804c656b91b8e9bb4f9a", + "cm-reverse-scenario.after-template/oa/behavior/scenario_oa_cm_200.arcadia": "0ec8d7e38f5a5cd56ccf3979757b3a9f412ad0ae36ee6c2690b7872129046d70", + "cm-reverse-scenario.after-template/traceability.arcadia": "dd5d356390089e4ec474374956f6219502733987e60ef8ff32920d8b33dce858", + "cm-reverse-scenario.arcadia": "1abc4c4cac37cfb06ed3b12a115f9e7e6a2d3affd34b25c355309e5ee465fa40", + "cm-reverse-scenario.json": "a5cf4cb2ef390113dd752f4054d43fb789da2ca77799efb5782805ae8aa2dfd5", + "cm-reverse-scenario.request.json": "c0ac99ee186d126fe3ec57f2c0a4072984d39d36ad21ead357ab66a08ac0065c", + "cm-transition-ready.arcadia": "b761c4c2af5f7064aaf1f887ffd6094eb6ae834b2c8057ecdbc4aa97c042066b", + "cm-transition-ready.json": "2536b000de361fb131787e32b466b9123d4e98abf00c099ad32f6cb8e7fb530b", + "generate-communication-means.rs": "003521404b702332082e01f2be7676fda1dd7b5bcf220bb81d25120c8984c75d", + "manifest.json": "fb921f42f2c4a4f8a67d1266f17f8c7750b6a2619554b713aa92b17918f0c43e", + "README.md": "015c16010c212af283f9def118c6d263ba65a3bc6e81a750f180624ca69bb311" + } +} diff --git a/harness/fixtures/release1-native/communication-means/manifest.json b/harness/fixtures/release1-native/communication-means/manifest.json new file mode 100644 index 00000000..4cef1d45 --- /dev/null +++ b/harness/fixtures/release1-native/communication-means/manifest.json @@ -0,0 +1,33 @@ +{ + "correlated_reply": "c6060906-0000-4000-8000-0000000000cc", + "draw_diagram": "c6060906-0000-4000-8000-0000000003e8", + "existing_ports": [ + "c6060906-0000-4000-8000-000000000020", + "c6060906-0000-4000-8000-000000000021" + ], + "interaction": "c6060906-0000-4000-8000-000000000016", + "native_status": "pending", + "oa_activities": [ + "c6060906-0000-4000-8000-00000000000c", + "c6060906-0000-4000-8000-00000000000d" + ], + "oa_participants": [ + "c6060906-0000-4000-8000-00000000000a", + "c6060906-0000-4000-8000-00000000000b" + ], + "participant_traces": [ + "c6060906-0000-4000-8000-000000000028", + "c6060906-0000-4000-8000-000000000029" + ], + "project": "c6060906-0000-4000-8000-000000000001", + "reverse_call": "c6060906-0000-4000-8000-0000000000cb", + "reverse_scenario": "c6060906-0000-4000-8000-0000000000c8", + "sa_participants": [ + "c6060906-0000-4000-8000-00000000001e", + "c6060906-0000-4000-8000-00000000001f" + ], + "schema": 6, + "system_diagram": "c6060906-0000-4000-8000-00000000044c", + "template_mean": "c6060906-0000-4000-8000-000000000032", + "templates": "Generated target/trace UUIDs are reference examples, not native exact expectations. Use check with native before/after sources." +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/dense-40/dense-40-oracle.json b/harness/fixtures/release1-native/dense-40/dense-40-oracle.json new file mode 100644 index 00000000..069824b8 --- /dev/null +++ b/harness/fixtures/release1-native/dense-40/dense-40-oracle.json @@ -0,0 +1,954 @@ +{ + "cases": { + "all": { + "count": 40, + "edges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007e3", + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007e6", + "d4060906-0000-4000-8000-0000000007e7", + "d4060906-0000-4000-8000-0000000007e8", + "d4060906-0000-4000-8000-0000000007e9", + "d4060906-0000-4000-8000-0000000007ea", + "d4060906-0000-4000-8000-0000000007eb", + "d4060906-0000-4000-8000-0000000007ec", + "d4060906-0000-4000-8000-0000000007ed", + "d4060906-0000-4000-8000-0000000007ee", + "d4060906-0000-4000-8000-0000000007ef", + "d4060906-0000-4000-8000-0000000007f0", + "d4060906-0000-4000-8000-0000000007f1", + "d4060906-0000-4000-8000-0000000007f2", + "d4060906-0000-4000-8000-0000000007f3", + "d4060906-0000-4000-8000-0000000007f4", + "d4060906-0000-4000-8000-0000000007f5", + "d4060906-0000-4000-8000-0000000007f6", + "d4060906-0000-4000-8000-0000000007f7" + ] + }, + "empty_categories": { + "count": 0, + "edges": [] + }, + "empty_contexts": { + "count": 0, + "edges": [] + }, + "first_and_overlap": { + "count": 13, + "edges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007ee" + ] + }, + "first_category": { + "count": 10, + "edges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007d9" + ] + }, + "first_hide_05": { + "count": 9, + "edges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007d9" + ] + }, + "in_behavior": { + "count": 30, + "edges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007e3", + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007e6", + "d4060906-0000-4000-8000-0000000007e7", + "d4060906-0000-4000-8000-0000000007e8", + "d4060906-0000-4000-8000-0000000007e9", + "d4060906-0000-4000-8000-0000000007ea", + "d4060906-0000-4000-8000-0000000007eb", + "d4060906-0000-4000-8000-0000000007ec", + "d4060906-0000-4000-8000-0000000007ed" + ] + }, + "no_lines": { + "count": 0, + "edges": [] + }, + "outside_behavior": { + "count": 10, + "edges": [ + "d4060906-0000-4000-8000-0000000007ee", + "d4060906-0000-4000-8000-0000000007ef", + "d4060906-0000-4000-8000-0000000007f0", + "d4060906-0000-4000-8000-0000000007f1", + "d4060906-0000-4000-8000-0000000007f2", + "d4060906-0000-4000-8000-0000000007f3", + "d4060906-0000-4000-8000-0000000007f4", + "d4060906-0000-4000-8000-0000000007f5", + "d4060906-0000-4000-8000-0000000007f6", + "d4060906-0000-4000-8000-0000000007f7" + ] + }, + "outside_second_process": { + "count": 0, + "edges": [] + }, + "second_process": { + "count": 10, + "edges": [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007e3" + ] + } + }, + "diagram": "d4060906-0000-4000-8000-000000000fa0", + "elements": 46, + "exactly_coincident_path_pairs": [ + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d2" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d3" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d4" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d7" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007da" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d3" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d4" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d7" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007da" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d4" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d7" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007da" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d4" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d7" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007da" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007d7" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007da" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007db" + ], + [ + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007e0" + ], + [ + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007e6" + ], + [ + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007eb" + ], + [ + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007f6" + ], + [ + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007dc" + ], + [ + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007e1" + ], + [ + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007e7" + ], + [ + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007ec" + ], + [ + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007f7" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007da" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007de" + ], + [ + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007e3" + ], + [ + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007e8" + ], + [ + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007ee" + ], + [ + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007f3" + ], + [ + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007df" + ], + [ + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007e4" + ], + [ + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007e9" + ], + [ + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007ef" + ], + [ + "d4060906-0000-4000-8000-0000000007d9", + "d4060906-0000-4000-8000-0000000007f4" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007dd" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007e0" + ], + [ + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007e6" + ], + [ + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007eb" + ], + [ + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007f6" + ], + [ + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007e1" + ], + [ + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007e7" + ], + [ + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007ec" + ], + [ + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007f7" + ], + [ + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007e2" + ], + [ + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007e3" + ], + [ + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007e8" + ], + [ + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007ee" + ], + [ + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007f3" + ], + [ + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007e4" + ], + [ + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007e9" + ], + [ + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007ef" + ], + [ + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007f4" + ], + [ + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007e6" + ], + [ + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007eb" + ], + [ + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007f6" + ], + [ + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007e7" + ], + [ + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007ec" + ], + [ + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007f7" + ], + [ + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007e5" + ], + [ + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007e3", + "d4060906-0000-4000-8000-0000000007e8" + ], + [ + "d4060906-0000-4000-8000-0000000007e3", + "d4060906-0000-4000-8000-0000000007ee" + ], + [ + "d4060906-0000-4000-8000-0000000007e3", + "d4060906-0000-4000-8000-0000000007f3" + ], + [ + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007e9" + ], + [ + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007ef" + ], + [ + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007f4" + ], + [ + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007ea" + ], + [ + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007e6", + "d4060906-0000-4000-8000-0000000007eb" + ], + [ + "d4060906-0000-4000-8000-0000000007e6", + "d4060906-0000-4000-8000-0000000007f6" + ], + [ + "d4060906-0000-4000-8000-0000000007e7", + "d4060906-0000-4000-8000-0000000007ec" + ], + [ + "d4060906-0000-4000-8000-0000000007e7", + "d4060906-0000-4000-8000-0000000007f7" + ], + [ + "d4060906-0000-4000-8000-0000000007e8", + "d4060906-0000-4000-8000-0000000007ee" + ], + [ + "d4060906-0000-4000-8000-0000000007e8", + "d4060906-0000-4000-8000-0000000007f3" + ], + [ + "d4060906-0000-4000-8000-0000000007e9", + "d4060906-0000-4000-8000-0000000007ef" + ], + [ + "d4060906-0000-4000-8000-0000000007e9", + "d4060906-0000-4000-8000-0000000007f4" + ], + [ + "d4060906-0000-4000-8000-0000000007ea", + "d4060906-0000-4000-8000-0000000007ed" + ], + [ + "d4060906-0000-4000-8000-0000000007ea", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007ea", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007eb", + "d4060906-0000-4000-8000-0000000007f6" + ], + [ + "d4060906-0000-4000-8000-0000000007ec", + "d4060906-0000-4000-8000-0000000007f7" + ], + [ + "d4060906-0000-4000-8000-0000000007ed", + "d4060906-0000-4000-8000-0000000007f0" + ], + [ + "d4060906-0000-4000-8000-0000000007ed", + "d4060906-0000-4000-8000-0000000007f5" + ], + [ + "d4060906-0000-4000-8000-0000000007ee", + "d4060906-0000-4000-8000-0000000007f3" + ], + [ + "d4060906-0000-4000-8000-0000000007ef", + "d4060906-0000-4000-8000-0000000007f4" + ], + [ + "d4060906-0000-4000-8000-0000000007f0", + "d4060906-0000-4000-8000-0000000007f5" + ] + ], + "label_overlap_pairs": [], + "native_readability": "pending; metrics do not establish a usability pass", + "project": "d4060906-0000-4000-8000-000000000001", + "relations": 42, + "routing_ms": 16, + "routing_warnings": [ + { + "edge": "d4060906-0000-4000-8000-0000000007d0", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d1", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d2", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d3", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d4", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d5", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d6", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d7", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d8", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007d9", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007da", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007db", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007dc", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007dd", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007de", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007df", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e0", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e1", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e2", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e3", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e4", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e5", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e6", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e7", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e8", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007e9", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007ea", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007eb", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007ec", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007ed", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007ee", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007ef", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f0", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f1", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f2", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f3", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f4", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f5", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f6", + "warnings": [] + }, + { + "edge": "d4060906-0000-4000-8000-0000000007f7", + "warnings": [] + } + ], + "schema": 6, + "semantic_oracles": "passed" +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/dense-40/dense-40.arcadia b/harness/fixtures/release1-native/dense-40/dense-40.arcadia new file mode 100644 index 00000000..27d1b797 --- /dev/null +++ b/harness/fixtures/release1-native/dense-40/dense-40.arcadia @@ -0,0 +1,956 @@ +arcadia 6; +project dense_service_views "Forty service exchanges" id "d4060906-0000-4000-8000-000000000001" { + description "Deterministic release fixture. Forty actual work exchanges share two endpoints; no authored routes or synthetic endpoints."; + + oa_entity requester "Requester" id "d4060906-0000-4000-8000-00000000000a" { + description ""; + parent none; + } + + oa_entity provider "Provider" id "d4060906-0000-4000-8000-00000000000b" { + description ""; + parent none; + } + + oa_activity request_service "Request service" id "d4060906-0000-4000-8000-000000000014" { + description ""; + parent none; + } + + oa_activity provide_service "Provide service" id "d4060906-0000-4000-8000-000000000015" { + description ""; + parent none; + } + + oa_exchange_item payload_1 "Commands payload" id "d4060906-0000-4000-8000-000000000064" { + description ""; + kind Information; + mechanism Unspecified; + parent none; + supertype none; + } + + oa_exchange_item payload_2 "Status payload" id "d4060906-0000-4000-8000-000000000065" { + description ""; + kind Information; + mechanism Unspecified; + parent none; + supertype none; + } + + oa_exchange_item payload_3 "Alerts payload" id "d4060906-0000-4000-8000-000000000066" { + description ""; + kind Information; + mechanism Unspecified; + parent none; + supertype none; + } + + oa_exchange_item payload_4 "Measurements payload" id "d4060906-0000-4000-8000-000000000067" { + description ""; + kind Information; + mechanism Unspecified; + parent none; + supertype none; + } + + oa_exchange_category concern_1 "Commands" id "d4060906-0000-4000-8000-0000000000c8" { + description ""; + exchanges [@service_data_01, @service_data_02, @service_data_03, @service_data_04, @service_data_05, @service_data_06, @service_data_07, @service_data_08, @service_data_09, @service_data_10]; + family "Work"; + parent none; + } + + oa_exchange_category concern_2 "Status" id "d4060906-0000-4000-8000-0000000000c9" { + description ""; + exchanges [@service_data_11, @service_data_12, @service_data_13, @service_data_14, @service_data_15, @service_data_16, @service_data_17, @service_data_18, @service_data_19, @service_data_20]; + family "Work"; + parent none; + } + + oa_exchange_category concern_3 "Alerts" id "d4060906-0000-4000-8000-0000000000ca" { + description ""; + exchanges [@service_data_21, @service_data_22, @service_data_23, @service_data_24, @service_data_25, @service_data_26, @service_data_27, @service_data_28, @service_data_29, @service_data_30]; + family "Work"; + parent none; + } + + oa_exchange_category concern_4 "Measurements" id "d4060906-0000-4000-8000-0000000000cb" { + description ""; + exchanges [@service_data_31, @service_data_32, @service_data_33, @service_data_34, @service_data_35, @service_data_36, @service_data_37, @service_data_38, @service_data_39, @service_data_40]; + family "Work"; + parent none; + } + + oa_exchange_category critical_concern "Critical cross-cutting exchanges" id "d4060906-0000-4000-8000-0000000000cc" { + description ""; + exchanges [@service_data_01, @service_data_11, @service_data_21, @service_data_31]; + family "Work"; + parent none; + } + + oa_process process_1 "Commands process" id "d4060906-0000-4000-8000-00000000012c" { + description ""; + parent none; + steps [@use_data_01, @use_data_02, @use_data_03, @use_data_04, @use_data_05, @use_data_06, @use_data_07, @use_data_08, @use_data_09, @use_data_10]; + } + + oa_process process_2 "Status process" id "d4060906-0000-4000-8000-00000000012d" { + description ""; + parent none; + steps [@use_data_11, @use_data_12, @use_data_13, @use_data_14, @use_data_15, @use_data_16, @use_data_17, @use_data_18, @use_data_19, @use_data_20]; + } + + oa_process process_3 "Alerts process" id "d4060906-0000-4000-8000-00000000012e" { + description ""; + parent none; + steps [@use_data_21, @use_data_22, @use_data_23, @use_data_24, @use_data_25, @use_data_26, @use_data_27, @use_data_28, @use_data_29, @use_data_30]; + } + + oa_occurrence use_data_01 "Use service data 01" id "d4060906-0000-4000-8000-0000000003e8" { + body { + Exchange { + exchange @service_data_01; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_02 "Use service data 02" id "d4060906-0000-4000-8000-0000000003e9" { + body { + Exchange { + exchange @service_data_02; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_03 "Use service data 03" id "d4060906-0000-4000-8000-0000000003ea" { + body { + Exchange { + exchange @service_data_03; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_04 "Use service data 04" id "d4060906-0000-4000-8000-0000000003eb" { + body { + Exchange { + exchange @service_data_04; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_05 "Use service data 05" id "d4060906-0000-4000-8000-0000000003ec" { + body { + Exchange { + exchange @service_data_05; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_06 "Use service data 06" id "d4060906-0000-4000-8000-0000000003ed" { + body { + Exchange { + exchange @service_data_06; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_07 "Use service data 07" id "d4060906-0000-4000-8000-0000000003ee" { + body { + Exchange { + exchange @service_data_07; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_08 "Use service data 08" id "d4060906-0000-4000-8000-0000000003ef" { + body { + Exchange { + exchange @service_data_08; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_09 "Use service data 09" id "d4060906-0000-4000-8000-0000000003f0" { + body { + Exchange { + exchange @service_data_09; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_10 "Use service data 10" id "d4060906-0000-4000-8000-0000000003f1" { + body { + Exchange { + exchange @service_data_10; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_1; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_11 "Use service data 11" id "d4060906-0000-4000-8000-0000000003f2" { + body { + Exchange { + exchange @service_data_11; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_12 "Use service data 12" id "d4060906-0000-4000-8000-0000000003f3" { + body { + Exchange { + exchange @service_data_12; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_13 "Use service data 13" id "d4060906-0000-4000-8000-0000000003f4" { + body { + Exchange { + exchange @service_data_13; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_14 "Use service data 14" id "d4060906-0000-4000-8000-0000000003f5" { + body { + Exchange { + exchange @service_data_14; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_15 "Use service data 15" id "d4060906-0000-4000-8000-0000000003f6" { + body { + Exchange { + exchange @service_data_15; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_16 "Use service data 16" id "d4060906-0000-4000-8000-0000000003f7" { + body { + Exchange { + exchange @service_data_16; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_17 "Use service data 17" id "d4060906-0000-4000-8000-0000000003f8" { + body { + Exchange { + exchange @service_data_17; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_18 "Use service data 18" id "d4060906-0000-4000-8000-0000000003f9" { + body { + Exchange { + exchange @service_data_18; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_19 "Use service data 19" id "d4060906-0000-4000-8000-0000000003fa" { + body { + Exchange { + exchange @service_data_19; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_20 "Use service data 20" id "d4060906-0000-4000-8000-0000000003fb" { + body { + Exchange { + exchange @service_data_20; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_2; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_21 "Use service data 21" id "d4060906-0000-4000-8000-0000000003fc" { + body { + Exchange { + exchange @service_data_21; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_22 "Use service data 22" id "d4060906-0000-4000-8000-0000000003fd" { + body { + Exchange { + exchange @service_data_22; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_23 "Use service data 23" id "d4060906-0000-4000-8000-0000000003fe" { + body { + Exchange { + exchange @service_data_23; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_24 "Use service data 24" id "d4060906-0000-4000-8000-0000000003ff" { + body { + Exchange { + exchange @service_data_24; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_25 "Use service data 25" id "d4060906-0000-4000-8000-000000000400" { + body { + Exchange { + exchange @service_data_25; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_26 "Use service data 26" id "d4060906-0000-4000-8000-000000000401" { + body { + Exchange { + exchange @service_data_26; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_27 "Use service data 27" id "d4060906-0000-4000-8000-000000000402" { + body { + Exchange { + exchange @service_data_27; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_28 "Use service data 28" id "d4060906-0000-4000-8000-000000000403" { + body { + Exchange { + exchange @service_data_28; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_29 "Use service data 29" id "d4060906-0000-4000-8000-000000000404" { + body { + Exchange { + exchange @service_data_29; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + oa_occurrence use_data_30 "Use service data 30" id "d4060906-0000-4000-8000-000000000405" { + body { + Exchange { + exchange @service_data_30; + message_kind Asynchronous; + receiver none; + reply_to none; + sender none; + values []; + }; + }; + description ""; + parent @process_3; + postcondition ""; + precondition ""; + } + + interaction service_data_01 "Service data 01" id "d4060906-0000-4000-8000-0000000007d0" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_02 "Service data 02" id "d4060906-0000-4000-8000-0000000007d1" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_03 "Service data 03" id "d4060906-0000-4000-8000-0000000007d2" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_04 "Service data 04" id "d4060906-0000-4000-8000-0000000007d3" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_05 "Service data 05" id "d4060906-0000-4000-8000-0000000007d4" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_06 "Service data 06" id "d4060906-0000-4000-8000-0000000007d5" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_07 "Service data 07" id "d4060906-0000-4000-8000-0000000007d6" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_08 "Service data 08" id "d4060906-0000-4000-8000-0000000007d7" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_09 "Service data 09" id "d4060906-0000-4000-8000-0000000007d8" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_10 "Service data 10" id "d4060906-0000-4000-8000-0000000007d9" { + description ""; + items [@payload_1]; + source @request_service; + target @provide_service; + } + + interaction service_data_11 "Service data 11" id "d4060906-0000-4000-8000-0000000007da" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_12 "Service data 12" id "d4060906-0000-4000-8000-0000000007db" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_13 "Service data 13" id "d4060906-0000-4000-8000-0000000007dc" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_14 "Service data 14" id "d4060906-0000-4000-8000-0000000007dd" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_15 "Service data 15" id "d4060906-0000-4000-8000-0000000007de" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_16 "Service data 16" id "d4060906-0000-4000-8000-0000000007df" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_17 "Service data 17" id "d4060906-0000-4000-8000-0000000007e0" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_18 "Service data 18" id "d4060906-0000-4000-8000-0000000007e1" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_19 "Service data 19" id "d4060906-0000-4000-8000-0000000007e2" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_20 "Service data 20" id "d4060906-0000-4000-8000-0000000007e3" { + description ""; + items [@payload_2]; + source @request_service; + target @provide_service; + } + + interaction service_data_21 "Service data 21" id "d4060906-0000-4000-8000-0000000007e4" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_22 "Service data 22" id "d4060906-0000-4000-8000-0000000007e5" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_23 "Service data 23" id "d4060906-0000-4000-8000-0000000007e6" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_24 "Service data 24" id "d4060906-0000-4000-8000-0000000007e7" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_25 "Service data 25" id "d4060906-0000-4000-8000-0000000007e8" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_26 "Service data 26" id "d4060906-0000-4000-8000-0000000007e9" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_27 "Service data 27" id "d4060906-0000-4000-8000-0000000007ea" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_28 "Service data 28" id "d4060906-0000-4000-8000-0000000007eb" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_29 "Service data 29" id "d4060906-0000-4000-8000-0000000007ec" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_30 "Service data 30" id "d4060906-0000-4000-8000-0000000007ed" { + description ""; + items [@payload_3]; + source @request_service; + target @provide_service; + } + + interaction service_data_31 "Service data 31" id "d4060906-0000-4000-8000-0000000007ee" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_32 "Service data 32" id "d4060906-0000-4000-8000-0000000007ef" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_33 "Service data 33" id "d4060906-0000-4000-8000-0000000007f0" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_34 "Service data 34" id "d4060906-0000-4000-8000-0000000007f1" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_35 "Service data 35" id "d4060906-0000-4000-8000-0000000007f2" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_36 "Service data 36" id "d4060906-0000-4000-8000-0000000007f3" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_37 "Service data 37" id "d4060906-0000-4000-8000-0000000007f4" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_38 "Service data 38" id "d4060906-0000-4000-8000-0000000007f5" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_39 "Service data 39" id "d4060906-0000-4000-8000-0000000007f6" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + interaction service_data_40 "Service data 40" id "d4060906-0000-4000-8000-0000000007f7" { + description ""; + items [@payload_4]; + source @request_service; + target @provide_service; + } + + allocation requester_work "Requester responsibility" id "d4060906-0000-4000-8000-000000000bb8" { + description ""; + source @requester; + target @request_service; + } + + allocation provider_work "Provider responsibility" id "d4060906-0000-4000-8000-000000000bb9" { + description ""; + source @provider; + target @provide_service; + } + + diagram forty_exchange_view "Forty service exchanges · all concerns" id "d4060906-0000-4000-8000-000000000fa0" { + description "Start with All. Compare first category, overlapping concerns, global coverage and chosen processes. No hand-routed edges."; + layer Oa; + projection FunctionalDataflow; + routes []; + view_options { + coverage "All"; + exchange_labels "Name"; + hidden_edges []; + participant_categories "All"; + relation_kinds "All"; + work_categories "All"; + work_contexts "All"; + }; + } + + node service_placement_1 id "d4060906-0000-4000-8000-000000001004" { + collapsed false; + diagram @forty_exchange_view; + element @request_service; + position { + x 80.0; + y 160.0; + }; + size { + height 150.0; + width 230.0; + }; + } + + node service_placement_2 id "d4060906-0000-4000-8000-000000001005" { + collapsed false; + diagram @forty_exchange_view; + element @provide_service; + position { + x 760.0; + y 160.0; + }; + size { + height 150.0; + width 230.0; + }; + } + +} diff --git a/harness/fixtures/release1-native/dense-40/dense-40.json b/harness/fixtures/release1-native/dense-40/dense-40.json new file mode 100644 index 00000000..27275f49 --- /dev/null +++ b/harness/fixtures/release1-native/dense-40/dense-40.json @@ -0,0 +1,1818 @@ +{ + "schema_version": 6, + "id": "d4060906-0000-4000-8000-000000000001", + "alias": "dense_service_views", + "name": "Forty service exchanges", + "description": "Deterministic release fixture. Forty actual work exchanges share two endpoints; no authored routes or synthetic endpoints.", + "elements": { + "d4060906-0000-4000-8000-00000000000a": { + "id": "d4060906-0000-4000-8000-00000000000a", + "alias": "requester", + "name": "Requester", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "d4060906-0000-4000-8000-00000000000b": { + "id": "d4060906-0000-4000-8000-00000000000b", + "alias": "provider", + "name": "Provider", + "description": "", + "parent": null, + "body": { + "Oa": "Entity" + } + }, + "d4060906-0000-4000-8000-000000000014": { + "id": "d4060906-0000-4000-8000-000000000014", + "alias": "request_service", + "name": "Request service", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "d4060906-0000-4000-8000-000000000015": { + "id": "d4060906-0000-4000-8000-000000000015", + "alias": "provide_service", + "name": "Provide service", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "d4060906-0000-4000-8000-000000000064": { + "id": "d4060906-0000-4000-8000-000000000064", + "alias": "payload_1", + "name": "Commands payload", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Unspecified", + "supertype": null + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000065": { + "id": "d4060906-0000-4000-8000-000000000065", + "alias": "payload_2", + "name": "Status payload", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Unspecified", + "supertype": null + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000066": { + "id": "d4060906-0000-4000-8000-000000000066", + "alias": "payload_3", + "name": "Alerts payload", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Unspecified", + "supertype": null + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000067": { + "id": "d4060906-0000-4000-8000-000000000067", + "alias": "payload_4", + "name": "Measurements payload", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeItem": { + "kind": "Information", + "mechanism": "Unspecified", + "supertype": null + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000000c8": { + "id": "d4060906-0000-4000-8000-0000000000c8", + "alias": "concern_1", + "name": "Commands", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "family": "Work", + "exchanges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007d1", + "d4060906-0000-4000-8000-0000000007d2", + "d4060906-0000-4000-8000-0000000007d3", + "d4060906-0000-4000-8000-0000000007d4", + "d4060906-0000-4000-8000-0000000007d5", + "d4060906-0000-4000-8000-0000000007d6", + "d4060906-0000-4000-8000-0000000007d7", + "d4060906-0000-4000-8000-0000000007d8", + "d4060906-0000-4000-8000-0000000007d9" + ] + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000000c9": { + "id": "d4060906-0000-4000-8000-0000000000c9", + "alias": "concern_2", + "name": "Status", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "family": "Work", + "exchanges": [ + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007db", + "d4060906-0000-4000-8000-0000000007dc", + "d4060906-0000-4000-8000-0000000007dd", + "d4060906-0000-4000-8000-0000000007de", + "d4060906-0000-4000-8000-0000000007df", + "d4060906-0000-4000-8000-0000000007e0", + "d4060906-0000-4000-8000-0000000007e1", + "d4060906-0000-4000-8000-0000000007e2", + "d4060906-0000-4000-8000-0000000007e3" + ] + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000000ca": { + "id": "d4060906-0000-4000-8000-0000000000ca", + "alias": "concern_3", + "name": "Alerts", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "family": "Work", + "exchanges": [ + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007e5", + "d4060906-0000-4000-8000-0000000007e6", + "d4060906-0000-4000-8000-0000000007e7", + "d4060906-0000-4000-8000-0000000007e8", + "d4060906-0000-4000-8000-0000000007e9", + "d4060906-0000-4000-8000-0000000007ea", + "d4060906-0000-4000-8000-0000000007eb", + "d4060906-0000-4000-8000-0000000007ec", + "d4060906-0000-4000-8000-0000000007ed" + ] + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000000cb": { + "id": "d4060906-0000-4000-8000-0000000000cb", + "alias": "concern_4", + "name": "Measurements", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "family": "Work", + "exchanges": [ + "d4060906-0000-4000-8000-0000000007ee", + "d4060906-0000-4000-8000-0000000007ef", + "d4060906-0000-4000-8000-0000000007f0", + "d4060906-0000-4000-8000-0000000007f1", + "d4060906-0000-4000-8000-0000000007f2", + "d4060906-0000-4000-8000-0000000007f3", + "d4060906-0000-4000-8000-0000000007f4", + "d4060906-0000-4000-8000-0000000007f5", + "d4060906-0000-4000-8000-0000000007f6", + "d4060906-0000-4000-8000-0000000007f7" + ] + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000000cc": { + "id": "d4060906-0000-4000-8000-0000000000cc", + "alias": "critical_concern", + "name": "Critical cross-cutting exchanges", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "ExchangeCategory": { + "family": "Work", + "exchanges": [ + "d4060906-0000-4000-8000-0000000007d0", + "d4060906-0000-4000-8000-0000000007da", + "d4060906-0000-4000-8000-0000000007e4", + "d4060906-0000-4000-8000-0000000007ee" + ] + } + } + } + } + }, + "d4060906-0000-4000-8000-00000000012c": { + "id": "d4060906-0000-4000-8000-00000000012c", + "alias": "process_1", + "name": "Commands process", + "description": "", + "parent": null, + "body": { + "Oa": { + "Process": { + "steps": [ + "d4060906-0000-4000-8000-0000000003e8", + "d4060906-0000-4000-8000-0000000003e9", + "d4060906-0000-4000-8000-0000000003ea", + "d4060906-0000-4000-8000-0000000003eb", + "d4060906-0000-4000-8000-0000000003ec", + "d4060906-0000-4000-8000-0000000003ed", + "d4060906-0000-4000-8000-0000000003ee", + "d4060906-0000-4000-8000-0000000003ef", + "d4060906-0000-4000-8000-0000000003f0", + "d4060906-0000-4000-8000-0000000003f1" + ] + } + } + } + }, + "d4060906-0000-4000-8000-00000000012d": { + "id": "d4060906-0000-4000-8000-00000000012d", + "alias": "process_2", + "name": "Status process", + "description": "", + "parent": null, + "body": { + "Oa": { + "Process": { + "steps": [ + "d4060906-0000-4000-8000-0000000003f2", + "d4060906-0000-4000-8000-0000000003f3", + "d4060906-0000-4000-8000-0000000003f4", + "d4060906-0000-4000-8000-0000000003f5", + "d4060906-0000-4000-8000-0000000003f6", + "d4060906-0000-4000-8000-0000000003f7", + "d4060906-0000-4000-8000-0000000003f8", + "d4060906-0000-4000-8000-0000000003f9", + "d4060906-0000-4000-8000-0000000003fa", + "d4060906-0000-4000-8000-0000000003fb" + ] + } + } + } + }, + "d4060906-0000-4000-8000-00000000012e": { + "id": "d4060906-0000-4000-8000-00000000012e", + "alias": "process_3", + "name": "Alerts process", + "description": "", + "parent": null, + "body": { + "Oa": { + "Process": { + "steps": [ + "d4060906-0000-4000-8000-0000000003fc", + "d4060906-0000-4000-8000-0000000003fd", + "d4060906-0000-4000-8000-0000000003fe", + "d4060906-0000-4000-8000-0000000003ff", + "d4060906-0000-4000-8000-000000000400", + "d4060906-0000-4000-8000-000000000401", + "d4060906-0000-4000-8000-000000000402", + "d4060906-0000-4000-8000-000000000403", + "d4060906-0000-4000-8000-000000000404", + "d4060906-0000-4000-8000-000000000405" + ] + } + } + } + }, + "d4060906-0000-4000-8000-0000000003e8": { + "id": "d4060906-0000-4000-8000-0000000003e8", + "alias": "use_data_01", + "name": "Use service data 01", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d0", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003e9": { + "id": "d4060906-0000-4000-8000-0000000003e9", + "alias": "use_data_02", + "name": "Use service data 02", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d1", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003ea": { + "id": "d4060906-0000-4000-8000-0000000003ea", + "alias": "use_data_03", + "name": "Use service data 03", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d2", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003eb": { + "id": "d4060906-0000-4000-8000-0000000003eb", + "alias": "use_data_04", + "name": "Use service data 04", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d3", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003ec": { + "id": "d4060906-0000-4000-8000-0000000003ec", + "alias": "use_data_05", + "name": "Use service data 05", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d4", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003ed": { + "id": "d4060906-0000-4000-8000-0000000003ed", + "alias": "use_data_06", + "name": "Use service data 06", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d5", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003ee": { + "id": "d4060906-0000-4000-8000-0000000003ee", + "alias": "use_data_07", + "name": "Use service data 07", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d6", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003ef": { + "id": "d4060906-0000-4000-8000-0000000003ef", + "alias": "use_data_08", + "name": "Use service data 08", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d7", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f0": { + "id": "d4060906-0000-4000-8000-0000000003f0", + "alias": "use_data_09", + "name": "Use service data 09", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d8", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f1": { + "id": "d4060906-0000-4000-8000-0000000003f1", + "alias": "use_data_10", + "name": "Use service data 10", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012c", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007d9", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f2": { + "id": "d4060906-0000-4000-8000-0000000003f2", + "alias": "use_data_11", + "name": "Use service data 11", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007da", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f3": { + "id": "d4060906-0000-4000-8000-0000000003f3", + "alias": "use_data_12", + "name": "Use service data 12", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007db", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f4": { + "id": "d4060906-0000-4000-8000-0000000003f4", + "alias": "use_data_13", + "name": "Use service data 13", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007dc", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f5": { + "id": "d4060906-0000-4000-8000-0000000003f5", + "alias": "use_data_14", + "name": "Use service data 14", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007dd", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f6": { + "id": "d4060906-0000-4000-8000-0000000003f6", + "alias": "use_data_15", + "name": "Use service data 15", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007de", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f7": { + "id": "d4060906-0000-4000-8000-0000000003f7", + "alias": "use_data_16", + "name": "Use service data 16", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007df", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f8": { + "id": "d4060906-0000-4000-8000-0000000003f8", + "alias": "use_data_17", + "name": "Use service data 17", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e0", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003f9": { + "id": "d4060906-0000-4000-8000-0000000003f9", + "alias": "use_data_18", + "name": "Use service data 18", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e1", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003fa": { + "id": "d4060906-0000-4000-8000-0000000003fa", + "alias": "use_data_19", + "name": "Use service data 19", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e2", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003fb": { + "id": "d4060906-0000-4000-8000-0000000003fb", + "alias": "use_data_20", + "name": "Use service data 20", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012d", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e3", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003fc": { + "id": "d4060906-0000-4000-8000-0000000003fc", + "alias": "use_data_21", + "name": "Use service data 21", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e4", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003fd": { + "id": "d4060906-0000-4000-8000-0000000003fd", + "alias": "use_data_22", + "name": "Use service data 22", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e5", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003fe": { + "id": "d4060906-0000-4000-8000-0000000003fe", + "alias": "use_data_23", + "name": "Use service data 23", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e6", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-0000000003ff": { + "id": "d4060906-0000-4000-8000-0000000003ff", + "alias": "use_data_24", + "name": "Use service data 24", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e7", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000400": { + "id": "d4060906-0000-4000-8000-000000000400", + "alias": "use_data_25", + "name": "Use service data 25", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e8", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000401": { + "id": "d4060906-0000-4000-8000-000000000401", + "alias": "use_data_26", + "name": "Use service data 26", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007e9", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000402": { + "id": "d4060906-0000-4000-8000-000000000402", + "alias": "use_data_27", + "name": "Use service data 27", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007ea", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000403": { + "id": "d4060906-0000-4000-8000-000000000403", + "alias": "use_data_28", + "name": "Use service data 28", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007eb", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000404": { + "id": "d4060906-0000-4000-8000-000000000404", + "alias": "use_data_29", + "name": "Use service data 29", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007ec", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + }, + "d4060906-0000-4000-8000-000000000405": { + "id": "d4060906-0000-4000-8000-000000000405", + "alias": "use_data_30", + "name": "Use service data 30", + "description": "", + "parent": "d4060906-0000-4000-8000-00000000012e", + "body": { + "Oa": { + "Shared": { + "Occurrence": { + "body": { + "Exchange": { + "exchange": "d4060906-0000-4000-8000-0000000007ed", + "sender": null, + "receiver": null, + "message_kind": "Asynchronous", + "reply_to": null, + "values": [] + } + }, + "precondition": "", + "postcondition": "" + } + } + } + } + } + }, + "relations": { + "d4060906-0000-4000-8000-0000000007d0": { + "id": "d4060906-0000-4000-8000-0000000007d0", + "alias": "service_data_01", + "name": "Service data 01", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d1": { + "id": "d4060906-0000-4000-8000-0000000007d1", + "alias": "service_data_02", + "name": "Service data 02", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d2": { + "id": "d4060906-0000-4000-8000-0000000007d2", + "alias": "service_data_03", + "name": "Service data 03", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d3": { + "id": "d4060906-0000-4000-8000-0000000007d3", + "alias": "service_data_04", + "name": "Service data 04", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d4": { + "id": "d4060906-0000-4000-8000-0000000007d4", + "alias": "service_data_05", + "name": "Service data 05", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d5": { + "id": "d4060906-0000-4000-8000-0000000007d5", + "alias": "service_data_06", + "name": "Service data 06", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d6": { + "id": "d4060906-0000-4000-8000-0000000007d6", + "alias": "service_data_07", + "name": "Service data 07", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d7": { + "id": "d4060906-0000-4000-8000-0000000007d7", + "alias": "service_data_08", + "name": "Service data 08", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d8": { + "id": "d4060906-0000-4000-8000-0000000007d8", + "alias": "service_data_09", + "name": "Service data 09", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007d9": { + "id": "d4060906-0000-4000-8000-0000000007d9", + "alias": "service_data_10", + "name": "Service data 10", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000064" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007da": { + "id": "d4060906-0000-4000-8000-0000000007da", + "alias": "service_data_11", + "name": "Service data 11", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007db": { + "id": "d4060906-0000-4000-8000-0000000007db", + "alias": "service_data_12", + "name": "Service data 12", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007dc": { + "id": "d4060906-0000-4000-8000-0000000007dc", + "alias": "service_data_13", + "name": "Service data 13", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007dd": { + "id": "d4060906-0000-4000-8000-0000000007dd", + "alias": "service_data_14", + "name": "Service data 14", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007de": { + "id": "d4060906-0000-4000-8000-0000000007de", + "alias": "service_data_15", + "name": "Service data 15", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007df": { + "id": "d4060906-0000-4000-8000-0000000007df", + "alias": "service_data_16", + "name": "Service data 16", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e0": { + "id": "d4060906-0000-4000-8000-0000000007e0", + "alias": "service_data_17", + "name": "Service data 17", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e1": { + "id": "d4060906-0000-4000-8000-0000000007e1", + "alias": "service_data_18", + "name": "Service data 18", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e2": { + "id": "d4060906-0000-4000-8000-0000000007e2", + "alias": "service_data_19", + "name": "Service data 19", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e3": { + "id": "d4060906-0000-4000-8000-0000000007e3", + "alias": "service_data_20", + "name": "Service data 20", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000065" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e4": { + "id": "d4060906-0000-4000-8000-0000000007e4", + "alias": "service_data_21", + "name": "Service data 21", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e5": { + "id": "d4060906-0000-4000-8000-0000000007e5", + "alias": "service_data_22", + "name": "Service data 22", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e6": { + "id": "d4060906-0000-4000-8000-0000000007e6", + "alias": "service_data_23", + "name": "Service data 23", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e7": { + "id": "d4060906-0000-4000-8000-0000000007e7", + "alias": "service_data_24", + "name": "Service data 24", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e8": { + "id": "d4060906-0000-4000-8000-0000000007e8", + "alias": "service_data_25", + "name": "Service data 25", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007e9": { + "id": "d4060906-0000-4000-8000-0000000007e9", + "alias": "service_data_26", + "name": "Service data 26", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007ea": { + "id": "d4060906-0000-4000-8000-0000000007ea", + "alias": "service_data_27", + "name": "Service data 27", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007eb": { + "id": "d4060906-0000-4000-8000-0000000007eb", + "alias": "service_data_28", + "name": "Service data 28", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007ec": { + "id": "d4060906-0000-4000-8000-0000000007ec", + "alias": "service_data_29", + "name": "Service data 29", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007ed": { + "id": "d4060906-0000-4000-8000-0000000007ed", + "alias": "service_data_30", + "name": "Service data 30", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000066" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007ee": { + "id": "d4060906-0000-4000-8000-0000000007ee", + "alias": "service_data_31", + "name": "Service data 31", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007ef": { + "id": "d4060906-0000-4000-8000-0000000007ef", + "alias": "service_data_32", + "name": "Service data 32", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f0": { + "id": "d4060906-0000-4000-8000-0000000007f0", + "alias": "service_data_33", + "name": "Service data 33", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f1": { + "id": "d4060906-0000-4000-8000-0000000007f1", + "alias": "service_data_34", + "name": "Service data 34", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f2": { + "id": "d4060906-0000-4000-8000-0000000007f2", + "alias": "service_data_35", + "name": "Service data 35", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f3": { + "id": "d4060906-0000-4000-8000-0000000007f3", + "alias": "service_data_36", + "name": "Service data 36", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f4": { + "id": "d4060906-0000-4000-8000-0000000007f4", + "alias": "service_data_37", + "name": "Service data 37", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f5": { + "id": "d4060906-0000-4000-8000-0000000007f5", + "alias": "service_data_38", + "name": "Service data 38", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f6": { + "id": "d4060906-0000-4000-8000-0000000007f6", + "alias": "service_data_39", + "name": "Service data 39", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-0000000007f7": { + "id": "d4060906-0000-4000-8000-0000000007f7", + "alias": "service_data_40", + "name": "Service data 40", + "description": "", + "body": { + "Interaction": { + "source": "d4060906-0000-4000-8000-000000000014", + "target": "d4060906-0000-4000-8000-000000000015", + "items": [ + "d4060906-0000-4000-8000-000000000067" + ] + } + } + }, + "d4060906-0000-4000-8000-000000000bb8": { + "id": "d4060906-0000-4000-8000-000000000bb8", + "alias": "requester_work", + "name": "Requester responsibility", + "description": "", + "body": { + "Allocation": { + "source": "d4060906-0000-4000-8000-00000000000a", + "target": "d4060906-0000-4000-8000-000000000014" + } + } + }, + "d4060906-0000-4000-8000-000000000bb9": { + "id": "d4060906-0000-4000-8000-000000000bb9", + "alias": "provider_work", + "name": "Provider responsibility", + "description": "", + "body": { + "Allocation": { + "source": "d4060906-0000-4000-8000-00000000000b", + "target": "d4060906-0000-4000-8000-000000000015" + } + } + } + }, + "diagrams": { + "d4060906-0000-4000-8000-000000000fa0": { + "id": "d4060906-0000-4000-8000-000000000fa0", + "alias": "forty_exchange_view", + "name": "Forty service exchanges · all concerns", + "description": "Start with All. Compare first category, overlapping concerns, global coverage and chosen processes. No hand-routed edges.", + "layer": "Oa", + "projection": "FunctionalDataflow", + "view_options": { + "hidden_edges": [], + "relation_kinds": "All", + "work_categories": "All", + "participant_categories": "All", + "coverage": "All", + "work_contexts": "All", + "exchange_labels": "Name" + }, + "nodes": { + "d4060906-0000-4000-8000-000000001004": { + "id": "d4060906-0000-4000-8000-000000001004", + "alias": "service_placement_1", + "element": "d4060906-0000-4000-8000-000000000014", + "position": { + "x": 80.0, + "y": 160.0 + }, + "size": { + "width": 230.0, + "height": 150.0 + }, + "collapsed": false + }, + "d4060906-0000-4000-8000-000000001005": { + "id": "d4060906-0000-4000-8000-000000001005", + "alias": "service_placement_2", + "element": "d4060906-0000-4000-8000-000000000015", + "position": { + "x": 760.0, + "y": 160.0 + }, + "size": { + "width": 230.0, + "height": 150.0 + }, + "collapsed": false + } + }, + "routes": [] + } + }, + "artifact_links": {}, + "traces": {} +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-attachments-native.md b/harness/fixtures/release1-native/semantic-attachments-native.md new file mode 100644 index 00000000..a16af10f --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments-native.md @@ -0,0 +1,11 @@ +# Semantic attachments native fixture + +Use a working copy of `semantic-attachments.arcadia`; preserve the supplied original and `.after.arcadia/.after.json` oracles. The generator prepared the final result with the current built core, and reparsed both source documents. Twelve elements, two relationships, four pre-existing traces, two final candidates, two owner reviews, no additional impacts. + +1. Open **Startup ownership rationale**, then Review semantics. In Attachments select **Owned children**, choose **OA Verify interlock** and **SA Verify interlock**, then **Use exact attachment pair**. Choose **Set final value** and set destination Parent to **SA Safety coordination**. This moves only the SA child. +2. In the batch trace picker open **Navigation endpoint rationale**. In Attachments select **Relationships at source**, choose **OA Request response association** and **SA Request response association**, then **Use exact attachment pair**. Choose **Set final value**, then choose final Source **SA Validated navigation request**. Leave the relationship Target untouched. +3. The combined preview should contain two decisions/two actual candidate records, with both owner rows included. Apply once. The resulting accepted project must equal `semantic-attachments.after.json`; source must equal `semantic-attachments.after.arcadia` including the leading comment. Undo once restores exact original model/source/history; Redo once restores the exact after state. +4. Before Apply, optionally open **Interlock direct parent rationale**, review Header Parent with **Keep target**. This must conflict with the ownership Candidate. Exclude the direct row to retain its decision and restore valid preview. Likewise **Association direct endpoint rationale**, Relation Source Keep, conflicts with the owner Incident Candidate; exclude it before comparing final oracle. +5. Candidate editors must name the actual destination record. Check Properties, Attachments, Reference pairs, exact History and all right-edge actions at 1100x700 and 1440x900. Switch rows and family repeatedly; authored drafts must remain independent. Checkpoint recovery must preserve inclusion, invalid raw input when present, mappings and decisions, and must name existing selected traces correctly. + +The supplied after oracle includes only the two Candidate decisions in steps1/2. Optional conflict rows must be excluded, and their mere opening/reference mapping must not create extra accepted histories. Artifacts and missing-record creation are outside this fixture. A separate FunctionalExchange port-impact acceptance fixture is still needed. diff --git a/harness/fixtures/release1-native/semantic-attachments.after.arcadia b/harness/fixtures/release1-native/semantic-attachments.after.arcadia new file mode 100644 index 00000000..fcc9a5d5 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.after.arcadia @@ -0,0 +1,223 @@ +// Preserve the allocation and endpoint rationale verbatim +arcadia 6; +project mission "Explicit child allocation and association endpoint review" id "c2e362bc-b4b4-4101-a98c-451c01fcb221" { + description ""; + + oa_data_type oa_navigation_request "OA Navigation request" id "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_data_type oa_validated_navigation_request "OA Validated navigation request" id "2055bb7c-923d-4cac-99e8-abb493b6fcea" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_activity oa_safety_coordination "OA Safety coordination" id "20a682b5-0384-4692-bd4f-8039a5fef194" { + description ""; + parent none; + } + + sa_data_type sa_validated_navigation_request "SA Validated navigation request" id "2d9d56f4-106f-4324-81ee-0177b4415d41" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + sa_data_type sa_route_response "SA Route response" id "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_data_type oa_route_response "OA Route response" id "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_activity oa_verify_interlock "OA Verify interlock" id "7f883080-856b-4494-a251-98ff90202de1" { + description ""; + parent @oa_startup_coordination; + } + + sa_data_type sa_navigation_request "SA Navigation request" id "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "c552a009-04b1-4cb2-95f9-c1aad0bf49b9" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "c8fbca56-14c7-40d7-a45b-29ef0291cb61" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "f4b1eb8d-1c6d-4824-959c-50b8e9f70638" { + description ""; + parent none; + } + + sa_function sa_verify_interlock "SA Verify interlock" id "fcc2dadb-1f39-406f-b001-51d0ae9a69f6" { + description ""; + parent @sa_safety_coordination; + } + + data_association oa_request_response_association "OA Request response association" id "7edceeb7-2a73-4698-815e-fb43fa9d5081" { + description ""; + source @oa_navigation_request; + source_maximum none; + source_minimum 0; + source_navigable false; + source_role ""; + target @oa_route_response; + target_maximum none; + target_minimum 0; + target_navigable false; + target_role ""; + } + + data_association sa_request_response_association "SA Request response association" id "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6" { + description ""; + source @sa_validated_navigation_request; + source_maximum none; + source_minimum 0; + source_navigable false; + source_role ""; + target @sa_route_response; + target_maximum none; + target_minimum 0; + target_navigable false; + target_role ""; + } + + trace navigation_endpoint_review "Navigation endpoint rationale" id "7c8604c8-79b1-46cd-bcfd-3177316a2e62" { + baseline { + oa { + description ""; + name "OA Navigation request"; + }; + sa { + description ""; + name "SA Navigation request"; + }; + semantic { + facets [{ + Incident { + end Source; + keys { + oa uuid "7edceeb7-2a73-4698-815e-fb43fa9d5081"; + sa uuid "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6"; + }; + review { + oa { + Value true; + }; + sa { + Value false; + }; + }; + }; + }]; + }; + }; + description ""; + kind Manual; + oa @oa_navigation_request; + sa @sa_navigation_request; + } + + trace interlock_direct_review "Interlock direct parent rationale" id "a4dde018-c9b3-4865-a492-a8806be5b885" { + baseline none; + description ""; + kind Manual; + oa @oa_verify_interlock; + sa @sa_verify_interlock; + } + + trace startup_ownership_review "Startup ownership rationale" id "bb01f0c1-7805-4ce0-bde3-4a690ece6105" { + baseline { + oa { + description ""; + name "OA Startup coordination"; + }; + sa { + description ""; + name "SA Startup coordination"; + }; + semantic { + facets [{ + Child { + keys { + oa uuid "7f883080-856b-4494-a251-98ff90202de1"; + sa uuid "fcc2dadb-1f39-406f-b001-51d0ae9a69f6"; + }; + review { + oa { + Value true; + }; + sa { + Value false; + }; + }; + }; + }]; + }; + }; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + + trace association_direct_review "Association direct endpoint rationale" id "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b" { + baseline none; + description ""; + kind Manual; + oa @oa_request_response_association; + sa @sa_request_response_association; + } + +} diff --git a/harness/fixtures/release1-native/semantic-attachments.after.json b/harness/fixtures/release1-native/semantic-attachments.after.json new file mode 100644 index 00000000..d29368ba --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.after.json @@ -0,0 +1,346 @@ +{ + "schema_version": 6, + "id": "c2e362bc-b4b4-4101-a98c-451c01fcb221", + "alias": "mission", + "name": "Explicit child allocation and association endpoint review", + "description": "", + "elements": { + "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc": { + "id": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "alias": "oa_navigation_request", + "name": "OA Navigation request", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "2055bb7c-923d-4cac-99e8-abb493b6fcea": { + "id": "2055bb7c-923d-4cac-99e8-abb493b6fcea", + "alias": "oa_validated_navigation_request", + "name": "OA Validated navigation request", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "20a682b5-0384-4692-bd4f-8039a5fef194": { + "id": "20a682b5-0384-4692-bd4f-8039a5fef194", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "2d9d56f4-106f-4324-81ee-0177b4415d41": { + "id": "2d9d56f4-106f-4324-81ee-0177b4415d41", + "alias": "sa_validated_navigation_request", + "name": "SA Validated navigation request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455": { + "id": "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455", + "alias": "sa_route_response", + "name": "SA Route response", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16": { + "id": "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16", + "alias": "oa_route_response", + "name": "OA Route response", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "7f883080-856b-4494-a251-98ff90202de1": { + "id": "7f883080-856b-4494-a251-98ff90202de1", + "alias": "oa_verify_interlock", + "name": "OA Verify interlock", + "description": "", + "parent": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "body": { + "Oa": "Activity" + } + }, + "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805": { + "id": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "alias": "sa_navigation_request", + "name": "SA Navigation request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "c552a009-04b1-4cb2-95f9-c1aad0bf49b9": { + "id": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c8fbca56-14c7-40d7-a45b-29ef0291cb61": { + "id": "c8fbca56-14c7-40d7-a45b-29ef0291cb61", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "f4b1eb8d-1c6d-4824-959c-50b8e9f70638": { + "id": "f4b1eb8d-1c6d-4824-959c-50b8e9f70638", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "fcc2dadb-1f39-406f-b001-51d0ae9a69f6": { + "id": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6", + "alias": "sa_verify_interlock", + "name": "SA Verify interlock", + "description": "", + "parent": "f4b1eb8d-1c6d-4824-959c-50b8e9f70638", + "body": { + "Sa": "Function" + } + } + }, + "relations": { + "7edceeb7-2a73-4698-815e-fb43fa9d5081": { + "id": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "alias": "oa_request_response_association", + "name": "OA Request response association", + "description": "", + "body": { + "DataAssociation": { + "source": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "target": "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16", + "source_role": "", + "target_role": "", + "source_minimum": 0, + "source_maximum": null, + "target_minimum": 0, + "target_maximum": null, + "source_navigable": false, + "target_navigable": false + } + } + }, + "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6": { + "id": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6", + "alias": "sa_request_response_association", + "name": "SA Request response association", + "description": "", + "body": { + "DataAssociation": { + "source": "2d9d56f4-106f-4324-81ee-0177b4415d41", + "target": "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455", + "source_role": "", + "target_role": "", + "source_minimum": 0, + "source_maximum": null, + "target_minimum": 0, + "target_maximum": null, + "source_navigable": false, + "target_navigable": false + } + } + } + }, + "diagrams": {}, + "artifact_links": {}, + "traces": { + "7c8604c8-79b1-46cd-bcfd-3177316a2e62": { + "id": "7c8604c8-79b1-46cd-bcfd-3177316a2e62", + "alias": "navigation_endpoint_review", + "name": "Navigation endpoint rationale", + "description": "", + "oa": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "sa": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "kind": "Manual", + "baseline": { + "oa": { + "name": "OA Navigation request", + "description": "" + }, + "sa": { + "name": "SA Navigation request", + "description": "" + }, + "semantic": { + "facets": [ + { + "Incident": { + "keys": { + "oa": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "sa": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6" + }, + "end": "Source", + "review": { + "oa": { + "Value": true + }, + "sa": { + "Value": false + } + } + } + } + ] + } + } + }, + "a4dde018-c9b3-4865-a492-a8806be5b885": { + "id": "a4dde018-c9b3-4865-a492-a8806be5b885", + "alias": "interlock_direct_review", + "name": "Interlock direct parent rationale", + "description": "", + "oa": "7f883080-856b-4494-a251-98ff90202de1", + "sa": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6", + "kind": "Manual", + "baseline": null + }, + "bb01f0c1-7805-4ce0-bde3-4a690ece6105": { + "id": "bb01f0c1-7805-4ce0-bde3-4a690ece6105", + "alias": "startup_ownership_review", + "name": "Startup ownership rationale", + "description": "", + "oa": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "sa": "c8fbca56-14c7-40d7-a45b-29ef0291cb61", + "kind": "Manual", + "baseline": { + "oa": { + "name": "OA Startup coordination", + "description": "" + }, + "sa": { + "name": "SA Startup coordination", + "description": "" + }, + "semantic": { + "facets": [ + { + "Child": { + "keys": { + "oa": "7f883080-856b-4494-a251-98ff90202de1", + "sa": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6" + }, + "review": { + "oa": { + "Value": true + }, + "sa": { + "Value": false + } + } + } + } + ] + } + } + }, + "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b": { + "id": "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b", + "alias": "association_direct_review", + "name": "Association direct endpoint rationale", + "description": "", + "oa": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "sa": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6", + "kind": "Manual", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-attachments.arcadia b/harness/fixtures/release1-native/semantic-attachments.arcadia new file mode 100644 index 00000000..cbefece7 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.arcadia @@ -0,0 +1,168 @@ +// Preserve the allocation and endpoint rationale verbatim +arcadia 6; +project mission "Explicit child allocation and association endpoint review" id "c2e362bc-b4b4-4101-a98c-451c01fcb221" { + description ""; + + oa_data_type oa_navigation_request "OA Navigation request" id "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_data_type oa_validated_navigation_request "OA Validated navigation request" id "2055bb7c-923d-4cac-99e8-abb493b6fcea" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_activity oa_safety_coordination "OA Safety coordination" id "20a682b5-0384-4692-bd4f-8039a5fef194" { + description ""; + parent none; + } + + sa_data_type sa_validated_navigation_request "SA Validated navigation request" id "2d9d56f4-106f-4324-81ee-0177b4415d41" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + sa_data_type sa_route_response "SA Route response" id "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_data_type oa_route_response "OA Route response" id "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_activity oa_verify_interlock "OA Verify interlock" id "7f883080-856b-4494-a251-98ff90202de1" { + description ""; + parent @oa_startup_coordination; + } + + sa_data_type sa_navigation_request "SA Navigation request" id "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805" { + default none; + definition { + Record { + supertype none; + }; + }; + description ""; + parent none; + unit ""; + } + + oa_activity oa_startup_coordination "OA Startup coordination" id "c552a009-04b1-4cb2-95f9-c1aad0bf49b9" { + description ""; + parent none; + } + + sa_function sa_startup_coordination "SA Startup coordination" id "c8fbca56-14c7-40d7-a45b-29ef0291cb61" { + description ""; + parent none; + } + + sa_function sa_safety_coordination "SA Safety coordination" id "f4b1eb8d-1c6d-4824-959c-50b8e9f70638" { + description ""; + parent none; + } + + sa_function sa_verify_interlock "SA Verify interlock" id "fcc2dadb-1f39-406f-b001-51d0ae9a69f6" { + description ""; + parent @sa_startup_coordination; + } + + data_association oa_request_response_association "OA Request response association" id "7edceeb7-2a73-4698-815e-fb43fa9d5081" { + description ""; + source @oa_navigation_request; + source_maximum none; + source_minimum 0; + source_navigable false; + source_role ""; + target @oa_route_response; + target_maximum none; + target_minimum 0; + target_navigable false; + target_role ""; + } + + data_association sa_request_response_association "SA Request response association" id "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6" { + description ""; + source @sa_navigation_request; + source_maximum none; + source_minimum 0; + source_navigable false; + source_role ""; + target @sa_route_response; + target_maximum none; + target_minimum 0; + target_navigable false; + target_role ""; + } + + trace navigation_endpoint_review "Navigation endpoint rationale" id "7c8604c8-79b1-46cd-bcfd-3177316a2e62" { + baseline none; + description ""; + kind Manual; + oa @oa_navigation_request; + sa @sa_navigation_request; + } + + trace interlock_direct_review "Interlock direct parent rationale" id "a4dde018-c9b3-4865-a492-a8806be5b885" { + baseline none; + description ""; + kind Manual; + oa @oa_verify_interlock; + sa @sa_verify_interlock; + } + + trace startup_ownership_review "Startup ownership rationale" id "bb01f0c1-7805-4ce0-bde3-4a690ece6105" { + baseline none; + description ""; + kind Manual; + oa @oa_startup_coordination; + sa @sa_startup_coordination; + } + + trace association_direct_review "Association direct endpoint rationale" id "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b" { + baseline none; + description ""; + kind Manual; + oa @oa_request_response_association; + sa @sa_request_response_association; + } + +} diff --git a/harness/fixtures/release1-native/semantic-attachments.hashes.json b/harness/fixtures/release1-native/semantic-attachments.hashes.json new file mode 100644 index 00000000..a141ee70 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.hashes.json @@ -0,0 +1,22 @@ +[ + { + "Algorithm": "SHA256", + "Hash": "8740F27BE99624013D9BE876639C0411E1B8C14F0F9A843F43DC71D769EFD79A", + "Path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-fixtures\\semantic-attachments.arcadia" + }, + { + "Algorithm": "SHA256", + "Hash": "9D084DD75A0251D349CC31F9EE98DF58D0BC76859DC3CB1503F242E2F759888A", + "Path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-fixtures\\semantic-attachments.after.arcadia" + }, + { + "Algorithm": "SHA256", + "Hash": "9C188F2890630C7FEEC2BE719A7104AF9690DA7EA836DF18FA78DA3FF2ACFC1B", + "Path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-fixtures\\semantic-attachments.json" + }, + { + "Algorithm": "SHA256", + "Hash": "F09BE41AFD85C18FD52FDF997E9B81C4B7CB2C41E78D167ABCD14CAEA8110333", + "Path": "C:\\Users\\User\\Documents\\Git\\ARCADIA-Studio\\.local\\native-fixtures\\semantic-attachments.after.json" + } +] diff --git a/harness/fixtures/release1-native/semantic-attachments.json b/harness/fixtures/release1-native/semantic-attachments.json new file mode 100644 index 00000000..8909a7d1 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.json @@ -0,0 +1,287 @@ +{ + "schema_version": 6, + "id": "c2e362bc-b4b4-4101-a98c-451c01fcb221", + "alias": "mission", + "name": "Explicit child allocation and association endpoint review", + "description": "", + "elements": { + "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc": { + "id": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "alias": "oa_navigation_request", + "name": "OA Navigation request", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "2055bb7c-923d-4cac-99e8-abb493b6fcea": { + "id": "2055bb7c-923d-4cac-99e8-abb493b6fcea", + "alias": "oa_validated_navigation_request", + "name": "OA Validated navigation request", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "20a682b5-0384-4692-bd4f-8039a5fef194": { + "id": "20a682b5-0384-4692-bd4f-8039a5fef194", + "alias": "oa_safety_coordination", + "name": "OA Safety coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "2d9d56f4-106f-4324-81ee-0177b4415d41": { + "id": "2d9d56f4-106f-4324-81ee-0177b4415d41", + "alias": "sa_validated_navigation_request", + "name": "SA Validated navigation request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455": { + "id": "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455", + "alias": "sa_route_response", + "name": "SA Route response", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16": { + "id": "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16", + "alias": "oa_route_response", + "name": "OA Route response", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "7f883080-856b-4494-a251-98ff90202de1": { + "id": "7f883080-856b-4494-a251-98ff90202de1", + "alias": "oa_verify_interlock", + "name": "OA Verify interlock", + "description": "", + "parent": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "body": { + "Oa": "Activity" + } + }, + "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805": { + "id": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "alias": "sa_navigation_request", + "name": "SA Navigation request", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Record": { + "supertype": null + } + }, + "default": null, + "unit": "" + } + } + } + } + }, + "c552a009-04b1-4cb2-95f9-c1aad0bf49b9": { + "id": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "alias": "oa_startup_coordination", + "name": "OA Startup coordination", + "description": "", + "parent": null, + "body": { + "Oa": "Activity" + } + }, + "c8fbca56-14c7-40d7-a45b-29ef0291cb61": { + "id": "c8fbca56-14c7-40d7-a45b-29ef0291cb61", + "alias": "sa_startup_coordination", + "name": "SA Startup coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "f4b1eb8d-1c6d-4824-959c-50b8e9f70638": { + "id": "f4b1eb8d-1c6d-4824-959c-50b8e9f70638", + "alias": "sa_safety_coordination", + "name": "SA Safety coordination", + "description": "", + "parent": null, + "body": { + "Sa": "Function" + } + }, + "fcc2dadb-1f39-406f-b001-51d0ae9a69f6": { + "id": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6", + "alias": "sa_verify_interlock", + "name": "SA Verify interlock", + "description": "", + "parent": "c8fbca56-14c7-40d7-a45b-29ef0291cb61", + "body": { + "Sa": "Function" + } + } + }, + "relations": { + "7edceeb7-2a73-4698-815e-fb43fa9d5081": { + "id": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "alias": "oa_request_response_association", + "name": "OA Request response association", + "description": "", + "body": { + "DataAssociation": { + "source": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "target": "7ddaaa50-946f-4ad9-ac82-e3cf60d6ef16", + "source_role": "", + "target_role": "", + "source_minimum": 0, + "source_maximum": null, + "target_minimum": 0, + "target_maximum": null, + "source_navigable": false, + "target_navigable": false + } + } + }, + "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6": { + "id": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6", + "alias": "sa_request_response_association", + "name": "SA Request response association", + "description": "", + "body": { + "DataAssociation": { + "source": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "target": "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455", + "source_role": "", + "target_role": "", + "source_minimum": 0, + "source_maximum": null, + "target_minimum": 0, + "target_maximum": null, + "source_navigable": false, + "target_navigable": false + } + } + } + }, + "diagrams": {}, + "artifact_links": {}, + "traces": { + "7c8604c8-79b1-46cd-bcfd-3177316a2e62": { + "id": "7c8604c8-79b1-46cd-bcfd-3177316a2e62", + "alias": "navigation_endpoint_review", + "name": "Navigation endpoint rationale", + "description": "", + "oa": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "sa": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "kind": "Manual", + "baseline": null + }, + "a4dde018-c9b3-4865-a492-a8806be5b885": { + "id": "a4dde018-c9b3-4865-a492-a8806be5b885", + "alias": "interlock_direct_review", + "name": "Interlock direct parent rationale", + "description": "", + "oa": "7f883080-856b-4494-a251-98ff90202de1", + "sa": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6", + "kind": "Manual", + "baseline": null + }, + "bb01f0c1-7805-4ce0-bde3-4a690ece6105": { + "id": "bb01f0c1-7805-4ce0-bde3-4a690ece6105", + "alias": "startup_ownership_review", + "name": "Startup ownership rationale", + "description": "", + "oa": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "sa": "c8fbca56-14c7-40d7-a45b-29ef0291cb61", + "kind": "Manual", + "baseline": null + }, + "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b": { + "id": "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b", + "alias": "association_direct_review", + "name": "Association direct endpoint rationale", + "description": "", + "oa": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "sa": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6", + "kind": "Manual", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-attachments.manifest.json b/harness/fixtures/release1-native/semantic-attachments.manifest.json new file mode 100644 index 00000000..5ec3312c --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.manifest.json @@ -0,0 +1,36 @@ +{ + "candidates": 2, + "child_facet": { + "Child": { + "oa": "7f883080-856b-4494-a251-98ff90202de1", + "sa": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6" + } + }, + "child_pair": [ + "7f883080-856b-4494-a251-98ff90202de1", + "fcc2dadb-1f39-406f-b001-51d0ae9a69f6" + ], + "child_trace": "a4dde018-c9b3-4865-a492-a8806be5b885", + "final_parent": "f4b1eb8d-1c6d-4824-959c-50b8e9f70638", + "final_source": "2d9d56f4-106f-4324-81ee-0177b4415d41", + "impacts": 0, + "incident_facet": { + "Incident": { + "end": "Source", + "keys": { + "oa": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "sa": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6" + } + } + }, + "incident_pair": [ + "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6" + ], + "owner_trace": "bb01f0c1-7805-4ce0-bde3-4a690ece6105", + "project": "c2e362bc-b4b4-4101-a98c-451c01fcb221", + "record_trace": "7c8604c8-79b1-46cd-bcfd-3177316a2e62", + "relation_trace": "e6cb8b03-36df-4df7-ae7e-7fe9463ab23b", + "reviews": 2, + "schema": 6 +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-attachments.request.json b/harness/fixtures/release1-native/semantic-attachments.request.json new file mode 100644 index 00000000..f64573e8 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-attachments.request.json @@ -0,0 +1,108 @@ +{ + "transition": { + "direction": "OaToSa", + "sources": [ + "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "target": "c8fbca56-14c7-40d7-a45b-29ef0291cb61", + "target_kind": null + }, + { + "source": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "target": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "target_kind": null + } + ], + "fields": [], + "resolutions": [], + "destination_diagram": null, + "communication_endpoints": [] + }, + "traces": { + "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc": "7c8604c8-79b1-46cd-bcfd-3177316a2e62", + "c552a009-04b1-4cb2-95f9-c1aad0bf49b9": "bb01f0c1-7805-4ce0-bde3-4a690ece6105" + }, + "references": { + "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc": "85b3f38d-5b88-4ec3-aa7c-1e9fbfdb0805", + "7edceeb7-2a73-4698-815e-fb43fa9d5081": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6", + "7f883080-856b-4494-a251-98ff90202de1": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6", + "c552a009-04b1-4cb2-95f9-c1aad0bf49b9": "c8fbca56-14c7-40d7-a45b-29ef0291cb61" + }, + "reviews": [ + { + "source": "c552a009-04b1-4cb2-95f9-c1aad0bf49b9", + "resolutions": [ + { + "facet": { + "Child": { + "oa": "7f883080-856b-4494-a251-98ff90202de1", + "sa": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6" + } + }, + "decision": "Candidate" + } + ], + "reset": [] + }, + { + "source": "0c5e45b7-cbcd-412a-ae74-f3a7cc6adafc", + "resolutions": [ + { + "facet": { + "Incident": { + "keys": { + "oa": "7edceeb7-2a73-4698-815e-fb43fa9d5081", + "sa": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6" + }, + "end": "Source" + } + }, + "decision": "Candidate" + } + ], + "reset": [] + } + ], + "candidates": [ + { + "Element": { + "id": "fcc2dadb-1f39-406f-b001-51d0ae9a69f6", + "alias": "sa_verify_interlock", + "name": "SA Verify interlock", + "description": "", + "parent": "f4b1eb8d-1c6d-4824-959c-50b8e9f70638", + "body": { + "Sa": "Function" + } + } + }, + { + "Relation": { + "id": "c7a6cb27-52d3-4c42-ad78-abd2aaf4dca6", + "alias": "sa_request_response_association", + "name": "SA Request response association", + "description": "", + "body": { + "DataAssociation": { + "source": "2d9d56f4-106f-4324-81ee-0177b4415d41", + "target": "737e07b8-ffe3-4fff-9fb0-ec8d0eaec455", + "source_role": "", + "target_role": "", + "source_minimum": 0, + "source_maximum": null, + "target_minimum": 0, + "target_maximum": null, + "source_navigable": false, + "target_navigable": false + } + } + } + } + ], + "accepted_impacts": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-batch-native.md b/harness/fixtures/release1-native/semantic-batch-native.md new file mode 100644 index 00000000..b8bec37e --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch-native.md @@ -0,0 +1,11 @@ +# Native multi-source semantic fixture + +Validated source6 fixture: `semantic-batch.arcadia` and `.json`. Copy the source to a separate native working location before saving. Initial model: three DataTypes, two traces, no relationships. The first line is an exact source-comment preservation sentinel. + +1. Open the fixture, choose Traceability, select **Normal range rationale**, then **Review semantics**. Normal operation limits maps to Controller admitted range. Select Integer Minimum, choose Set final value, enter `120`. Initial maximum100 makes this single-row proposal invalid; Apply must be disabled. +2. In **Choose another trace to add or reopen**, choose **Emergency range rationale**, then **Open row**. There must be two included rows, both targeting Controller admitted range. Select Integer Maximum, choose Set final value and enter `-`. The raw invalid input must remain visible and Apply disabled. +3. Optional exclusion/recovery falsifier before final repair: uncheck Include this row, switch to Normal via the retained-row picker and temporarily change minimum to `5`. One included row is now valid. Open the already retained Emergency row again; its `-` must remain and it must remain excluded, while the valid included preview remains available. Checkpoint/recover if exercising workflow recovery. Do not Apply this intermediate state. Return Normal minimum to `120`, reopen Emergency, include it and repair maximum to `150`. +4. With Normal minimum120 and Emergency maximum150 included, the single combined preview must become valid although Normal's independent raw group still retains base maximum100. Apply once. The shared target is exactly120..150 degrees; each trace records its own one-bound observation. Both source records stay0..100. One Undo restores all initial model/history/source; one Redo restores the exact after oracle. +5. Save/reopen and compare parsed Project with `semantic-batch.after.json`; compare exact saved source with `semantic-batch.after.arcadia`. The after-source file was produced by core `source_project::update_bundle` on the original source, not by reformatting the final Project. Check 1100x700 and1440x900 for retained-row picker, inclusion, property controls, error text and Apply visibility; right-side scrollers have18px gutters. + +Exact trace IDs and target are in `semantic-batch.manifest.json`. `semantic-batch.request.json` is an independently constructed core request with two source reviews, one complete target candidate and zero additional impacts. `semantic-batch.hashes.json` records initial/after golden hashes. No native acceptance is claimed by fixture generation. diff --git a/harness/fixtures/release1-native/semantic-batch.after.arcadia b/harness/fixtures/release1-native/semantic-batch.after.arcadia new file mode 100644 index 00000000..c5650e93 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.after.arcadia @@ -0,0 +1,107 @@ +// Keep this independent engineering explanation verbatim +arcadia 6; +project mission "Independent controller-limit reviews" id "4f043ea4-04fa-419a-867d-a52836290faa" { + description ""; + + oa_data_type emergency_operation_limits "Emergency operation limits" id "298094d9-75f3-4299-af3c-5fdfdff5aa0f" { + default none; + definition { + Integer { + maximum 100; + minimum 0; + }; + }; + description ""; + parent none; + unit "degrees"; + } + + sa_data_type controller_admitted_range "Controller admitted range" id "5b43cc68-555b-4c9b-823a-660c221514a6" { + default none; + definition { + Integer { + maximum 150; + minimum 120; + }; + }; + description ""; + parent none; + unit "degrees"; + } + + oa_data_type normal_operation_limits "Normal operation limits" id "6e7b075b-94a9-43f0-a18b-ee3d60884119" { + default none; + definition { + Integer { + maximum 100; + minimum 0; + }; + }; + description ""; + parent none; + unit "degrees"; + } + + trace normal_range_review "Normal range rationale" id "0c923a4f-cbfd-44d0-9e48-203ede7f0b07" { + baseline { + oa { + description ""; + name "Normal operation limits"; + }; + sa { + description ""; + name "Controller admitted range"; + }; + semantic { + facets [{ + Data { + IntegerMinimum { + oa { + Value 0; + }; + sa { + Value 120; + }; + }; + }; + }]; + }; + }; + description "Independently authored bound decision"; + kind Manual; + oa @normal_operation_limits; + sa @controller_admitted_range; + } + + trace emergency_range_review "Emergency range rationale" id "431bc20b-3441-4bf4-864b-902388ed9504" { + baseline { + oa { + description ""; + name "Emergency operation limits"; + }; + sa { + description ""; + name "Controller admitted range"; + }; + semantic { + facets [{ + Data { + IntegerMaximum { + oa { + Value 100; + }; + sa { + Value 150; + }; + }; + }; + }]; + }; + }; + description "Independently authored bound decision"; + kind Manual; + oa @emergency_operation_limits; + sa @controller_admitted_range; + } + +} diff --git a/harness/fixtures/release1-native/semantic-batch.after.json b/harness/fixtures/release1-native/semantic-batch.after.json new file mode 100644 index 00000000..9f7ae13b --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.after.json @@ -0,0 +1,153 @@ +{ + "schema_version": 6, + "id": "4f043ea4-04fa-419a-867d-a52836290faa", + "alias": "mission", + "name": "Independent controller-limit reviews", + "description": "", + "elements": { + "298094d9-75f3-4299-af3c-5fdfdff5aa0f": { + "id": "298094d9-75f3-4299-af3c-5fdfdff5aa0f", + "alias": "emergency_operation_limits", + "name": "Emergency operation limits", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 0, + "maximum": 100 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + }, + "5b43cc68-555b-4c9b-823a-660c221514a6": { + "id": "5b43cc68-555b-4c9b-823a-660c221514a6", + "alias": "controller_admitted_range", + "name": "Controller admitted range", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 120, + "maximum": 150 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + }, + "6e7b075b-94a9-43f0-a18b-ee3d60884119": { + "id": "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "alias": "normal_operation_limits", + "name": "Normal operation limits", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 0, + "maximum": 100 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": {}, + "traces": { + "0c923a4f-cbfd-44d0-9e48-203ede7f0b07": { + "id": "0c923a4f-cbfd-44d0-9e48-203ede7f0b07", + "alias": "normal_range_review", + "name": "Normal range rationale", + "description": "Independently authored bound decision", + "oa": "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "sa": "5b43cc68-555b-4c9b-823a-660c221514a6", + "kind": "Manual", + "baseline": { + "oa": { + "name": "Normal operation limits", + "description": "" + }, + "sa": { + "name": "Controller admitted range", + "description": "" + }, + "semantic": { + "facets": [ + { + "Data": { + "IntegerMinimum": { + "oa": { + "Value": 0 + }, + "sa": { + "Value": 120 + } + } + } + } + ] + } + } + }, + "431bc20b-3441-4bf4-864b-902388ed9504": { + "id": "431bc20b-3441-4bf4-864b-902388ed9504", + "alias": "emergency_range_review", + "name": "Emergency range rationale", + "description": "Independently authored bound decision", + "oa": "298094d9-75f3-4299-af3c-5fdfdff5aa0f", + "sa": "5b43cc68-555b-4c9b-823a-660c221514a6", + "kind": "Manual", + "baseline": { + "oa": { + "name": "Emergency operation limits", + "description": "" + }, + "sa": { + "name": "Controller admitted range", + "description": "" + }, + "semantic": { + "facets": [ + { + "Data": { + "IntegerMaximum": { + "oa": { + "Value": 100 + }, + "sa": { + "Value": 150 + } + } + } + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-batch.arcadia b/harness/fixtures/release1-native/semantic-batch.arcadia new file mode 100644 index 00000000..36e82356 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.arcadia @@ -0,0 +1,61 @@ +// Keep this independent engineering explanation verbatim +arcadia 6; +project mission "Independent controller-limit reviews" id "4f043ea4-04fa-419a-867d-a52836290faa" { + description ""; + + oa_data_type emergency_operation_limits "Emergency operation limits" id "298094d9-75f3-4299-af3c-5fdfdff5aa0f" { + default none; + definition { + Integer { + maximum 100; + minimum 0; + }; + }; + description ""; + parent none; + unit "degrees"; + } + + sa_data_type controller_admitted_range "Controller admitted range" id "5b43cc68-555b-4c9b-823a-660c221514a6" { + default none; + definition { + Integer { + maximum 100; + minimum 0; + }; + }; + description ""; + parent none; + unit "degrees"; + } + + oa_data_type normal_operation_limits "Normal operation limits" id "6e7b075b-94a9-43f0-a18b-ee3d60884119" { + default none; + definition { + Integer { + maximum 100; + minimum 0; + }; + }; + description ""; + parent none; + unit "degrees"; + } + + trace normal_range_review "Normal range rationale" id "0c923a4f-cbfd-44d0-9e48-203ede7f0b07" { + baseline none; + description "Independently authored bound decision"; + kind Manual; + oa @normal_operation_limits; + sa @controller_admitted_range; + } + + trace emergency_range_review "Emergency range rationale" id "431bc20b-3441-4bf4-864b-902388ed9504" { + baseline none; + description "Independently authored bound decision"; + kind Manual; + oa @emergency_operation_limits; + sa @controller_admitted_range; + } + +} diff --git a/harness/fixtures/release1-native/semantic-batch.hashes.json b/harness/fixtures/release1-native/semantic-batch.hashes.json new file mode 100644 index 00000000..03fcd81e --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.hashes.json @@ -0,0 +1,32 @@ +[ + { + "file": "semantic-batch.arcadia", + "bytes": 1787, + "sha256": "5e1f433e04621424edac43e53411624059e873f88dd531bfdb39093953485ed7" + }, + { + "file": "semantic-batch.json", + "bytes": 2816, + "sha256": "1972242d2a8677d004dc3af1081feb292de65fe26128d877dded8661f9906619" + }, + { + "file": "semantic-batch.after.arcadia", + "bytes": 3134, + "sha256": "d3315bba762395205ae761e6e64029e8b4070c15a1299c6114075581db7b0974" + }, + { + "file": "semantic-batch.after.json", + "bytes": 3927, + "sha256": "8f36119c7e893a4af374b13b85b1c2cb3df0196df8d06a551386628fc8b8646d" + }, + { + "file": "semantic-batch.request.json", + "bytes": 2197, + "sha256": "71f23474614aab90398ce24f00f2cf61d6cb9a3a34faa3e12e0c8104e0a71073" + }, + { + "file": "semantic-batch.manifest.json", + "bytes": 647, + "sha256": "1e440efa02e2ccc27585d9a49c7864d53e3e3e7645ccb219f040e00d95331b8e" + } +] diff --git a/harness/fixtures/release1-native/semantic-batch.json b/harness/fixtures/release1-native/semantic-batch.json new file mode 100644 index 00000000..05c80b55 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.json @@ -0,0 +1,103 @@ +{ + "schema_version": 6, + "id": "4f043ea4-04fa-419a-867d-a52836290faa", + "alias": "mission", + "name": "Independent controller-limit reviews", + "description": "", + "elements": { + "298094d9-75f3-4299-af3c-5fdfdff5aa0f": { + "id": "298094d9-75f3-4299-af3c-5fdfdff5aa0f", + "alias": "emergency_operation_limits", + "name": "Emergency operation limits", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 0, + "maximum": 100 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + }, + "5b43cc68-555b-4c9b-823a-660c221514a6": { + "id": "5b43cc68-555b-4c9b-823a-660c221514a6", + "alias": "controller_admitted_range", + "name": "Controller admitted range", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 0, + "maximum": 100 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + }, + "6e7b075b-94a9-43f0-a18b-ee3d60884119": { + "id": "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "alias": "normal_operation_limits", + "name": "Normal operation limits", + "description": "", + "parent": null, + "body": { + "Oa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 0, + "maximum": 100 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + } + }, + "relations": {}, + "diagrams": {}, + "artifact_links": {}, + "traces": { + "0c923a4f-cbfd-44d0-9e48-203ede7f0b07": { + "id": "0c923a4f-cbfd-44d0-9e48-203ede7f0b07", + "alias": "normal_range_review", + "name": "Normal range rationale", + "description": "Independently authored bound decision", + "oa": "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "sa": "5b43cc68-555b-4c9b-823a-660c221514a6", + "kind": "Manual", + "baseline": null + }, + "431bc20b-3441-4bf4-864b-902388ed9504": { + "id": "431bc20b-3441-4bf4-864b-902388ed9504", + "alias": "emergency_range_review", + "name": "Emergency range rationale", + "description": "Independently authored bound decision", + "oa": "298094d9-75f3-4299-af3c-5fdfdff5aa0f", + "sa": "5b43cc68-555b-4c9b-823a-660c221514a6", + "kind": "Manual", + "baseline": null + } + } +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-batch.manifest.json b/harness/fixtures/release1-native/semantic-batch.manifest.json new file mode 100644 index 00000000..232a81b5 --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.manifest.json @@ -0,0 +1,25 @@ +{ + "accepted_impacts": 0, + "core_candidates": 1, + "core_reviews": 2, + "exact_source_oracle": "source_project::update_bundle", + "final_maximum": 150, + "final_minimum": 120, + "initial_maximum": 100, + "initial_minimum": 0, + "project": "4f043ea4-04fa-419a-867d-a52836290faa", + "schema": 6, + "sources": [ + "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "298094d9-75f3-4299-af3c-5fdfdff5aa0f" + ], + "target": "5b43cc68-555b-4c9b-823a-660c221514a6", + "trace_names": [ + "Normal range rationale", + "Emergency range rationale" + ], + "traces": [ + "0c923a4f-cbfd-44d0-9e48-203ede7f0b07", + "431bc20b-3441-4bf4-864b-902388ed9504" + ] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-batch.request.json b/harness/fixtures/release1-native/semantic-batch.request.json new file mode 100644 index 00000000..76f7e21e --- /dev/null +++ b/harness/fixtures/release1-native/semantic-batch.request.json @@ -0,0 +1,88 @@ +{ + "transition": { + "direction": "OaToSa", + "sources": [ + "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "298094d9-75f3-4299-af3c-5fdfdff5aa0f" + ], + "scope": "SelectedOnly", + "mappings": [ + { + "source": "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "target": "5b43cc68-555b-4c9b-823a-660c221514a6", + "target_kind": null + }, + { + "source": "298094d9-75f3-4299-af3c-5fdfdff5aa0f", + "target": "5b43cc68-555b-4c9b-823a-660c221514a6", + "target_kind": null + } + ], + "fields": [], + "resolutions": [], + "destination_diagram": null, + "communication_endpoints": [] + }, + "traces": { + "298094d9-75f3-4299-af3c-5fdfdff5aa0f": "431bc20b-3441-4bf4-864b-902388ed9504", + "6e7b075b-94a9-43f0-a18b-ee3d60884119": "0c923a4f-cbfd-44d0-9e48-203ede7f0b07" + }, + "references": { + "298094d9-75f3-4299-af3c-5fdfdff5aa0f": "5b43cc68-555b-4c9b-823a-660c221514a6", + "6e7b075b-94a9-43f0-a18b-ee3d60884119": "5b43cc68-555b-4c9b-823a-660c221514a6" + }, + "reviews": [ + { + "source": "6e7b075b-94a9-43f0-a18b-ee3d60884119", + "resolutions": [ + { + "facet": { + "Data": "IntegerMinimum" + }, + "decision": "Candidate" + } + ], + "reset": [] + }, + { + "source": "298094d9-75f3-4299-af3c-5fdfdff5aa0f", + "resolutions": [ + { + "facet": { + "Data": "IntegerMaximum" + }, + "decision": "Candidate" + } + ], + "reset": [] + } + ], + "candidates": [ + { + "Element": { + "id": "5b43cc68-555b-4c9b-823a-660c221514a6", + "alias": "controller_admitted_range", + "name": "Controller admitted range", + "description": "", + "parent": null, + "body": { + "Sa": { + "Shared": { + "DataType": { + "definition": { + "Integer": { + "minimum": 120, + "maximum": 150 + } + }, + "default": null, + "unit": "degrees" + } + } + } + } + } + } + ], + "accepted_impacts": [] +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/semantic-keyed.steps.md b/harness/fixtures/release1-native/semantic-keyed.steps.md index 0376311b..8cd43283 100644 --- a/harness/fixtures/release1-native/semantic-keyed.steps.md +++ b/harness/fixtures/release1-native/semantic-keyed.steps.md @@ -63,4 +63,4 @@ Source oracles were generated through source_project::update_bundle, retaining unmodified declarations and the fixture comment, rather than replacing the whole file with a freshly formatted model. Generator source is generate-semantic-keyed.rs; it reuses the production-tested r3_semantic payload/scenario-use constructors. -Standalone rustc output stays in this directory; Cargo and shared outputs untouched. +Standalone rustc output stays in this directory; Cargo and shared outputs untouched. diff --git a/harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json b/harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json new file mode 100644 index 00000000..9418d8fa --- /dev/null +++ b/harness/fixtures/release1-native/source5-recovery/15000000-0000-4000-8000-000000000010.json @@ -0,0 +1,38 @@ +{ + "version": 1, + "id": "15000000-0000-4000-8000-000000000010", + "name": "Source 5 recovery acceptance", + "origin": null, + "modified": 1788750000, + "accepted": { + "entry": "project.arcadia", + "files": { + "project.arcadia": "// Exact source-5 recovery fixture: Unicode 雨, CRLF, and comment number 5.\r\narcadia // preserve header comment\r\n 5;\r\nproject p \"Source 5 recovery acceptance\" id \"15000000-0000-4000-8000-000000000001\" {\r\n include \"work.arcadia\";\r\n}\r\n", + "work.arcadia": "// Preserve this fragment exactly.\r\noa_activity a \"Retained work\" id \"15000000-0000-4000-8000-000000000002\" {}\r\n" + } + }, + "workspace": { + "entry": "project.arcadia", + "active": "project.arcadia", + "files": { + "project.arcadia": [ + "// Exact source-5 recovery fixture: Unicode 雨, CRLF, and comment number 5.\r\narcadia // preserve header comment\r\n 5;\r\nproject p \"Source 5 recovery acceptance\" id \"15000000-0000-4000-8000-000000000001\" {\r\n include \"work.arcadia\";\r\n}\r\n\r\nunfinished { 雨", + "// Exact source-5 recovery fixture: Unicode 雨, CRLF, and comment number 5.\r\narcadia // preserve header comment\r\n 5;\r\nproject p \"Source 5 recovery acceptance\" id \"15000000-0000-4000-8000-000000000001\" {\r\n include \"work.arcadia\";\r\n}\r\n" + ], + "work.arcadia": [ + "// Preserve this fragment exactly.\r\noa_activity a \"Retained work\" id \"15000000-0000-4000-8000-000000000002\" {}\r\n", + "// Preserve this fragment exactly.\r\noa_activity a \"Retained work\" id \"15000000-0000-4000-8000-000000000002\" {}\r\n" + ] + }, + "reachable": [ + "project.arcadia", + "work.arcadia" + ], + "applied": true, + "new_path": "drafts/retained-name.arcadia" + }, + "property": null, + "behavior": null, + "workflow": null, + "selected": "15000000-0000-4000-8000-000000000001" +} \ No newline at end of file diff --git a/harness/fixtures/release1-native/source5-recovery/generate.py b/harness/fixtures/release1-native/source5-recovery/generate.py new file mode 100644 index 00000000..1385f8e7 --- /dev/null +++ b/harness/fixtures/release1-native/source5-recovery/generate.py @@ -0,0 +1,34 @@ +"""Prepare a task-local checkpoint only; never installs it in user recovery data.""" +import json +from pathlib import Path + +entry = ( + '// Exact source-5 recovery fixture: Unicode 雨, CRLF, and comment number 5.\r\n' + 'arcadia // preserve header comment\r\n 5;\r\n' + 'project p "Source 5 recovery acceptance" id "15000000-0000-4000-8000-000000000001" {\r\n' + ' include "work.arcadia";\r\n}\r\n' +) +work = ( + '// Preserve this fragment exactly.\r\n' + 'oa_activity a "Retained work" id "15000000-0000-4000-8000-000000000002" {}\r\n' +) +snapshot = { + 'version': 1, + 'id': '15000000-0000-4000-8000-000000000010', + 'name': 'Source 5 recovery acceptance', + 'origin': None, + 'modified': 1788750000, + 'accepted': {'entry': 'project.arcadia', 'files': {'project.arcadia': entry, 'work.arcadia': work}}, + 'workspace': { + 'entry': 'project.arcadia', 'active': 'project.arcadia', + 'files': {'project.arcadia': [entry + '\r\nunfinished { 雨', entry], 'work.arcadia': [work, work]}, + 'reachable': ['project.arcadia', 'work.arcadia'], 'applied': True, + 'new_path': 'drafts/retained-name.arcadia', + }, + 'property': None, 'behavior': None, 'workflow': None, + 'selected': '15000000-0000-4000-8000-000000000001', +} +path = Path('.local/source5-native-recovery-fixture') / (snapshot['id'] + '.json') +path.parent.mkdir(exist_ok=True) +path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2), encoding='utf-8') +print(path.resolve()) diff --git a/harness/fixtures/rest-coverage/README.md b/harness/fixtures/rest-coverage/README.md index aea19799..df5e16d4 100644 --- a/harness/fixtures/rest-coverage/README.md +++ b/harness/fixtures/rest-coverage/README.md @@ -2,7 +2,7 @@ Run with PowerShell7 on Windows against a built/staged server. The script launches its own hidden server on a free loopback port, creates synthetic data under a fresh -timestamped `.local/rest-coverage-*` directory, captures25 requests/responses and +timestamped `.local/rest-coverage-*` directory, captures 25 requests/responses and asserts diagnostic parity, invalid-write rejection, source preservation and restart. It stops only its own process, including on failure. It never attaches to an existing server and does not use user credentials. The token it creates is fixed diff --git a/harness/history/2026-09-06-ledgers-before-consolidation/README.md b/harness/history/2026-09-06-ledgers-before-consolidation/README.md new file mode 100644 index 00000000..4fe77de5 --- /dev/null +++ b/harness/history/2026-09-06-ledgers-before-consolidation/README.md @@ -0,0 +1,12 @@ +# Ledger history preserved before consolidation + +`STATE.md` and `TASK_LEDGER.md` are byte-for-byte copies of the complete ledgers +immediately before the 2026-09-06 evening consolidation. Copy hashes were compared +with both originals before replacement. They preserve all historical requirements, +decisions, platform/branch evidence, old process state and superseded open work. + +These files are archival, not current instructions or status. Their original +relative evidence paths are relative to `harness/`, not this archive directory; +historical absolute paths may refer to the previous machine. Read the current +[Progress Ledger](../../STATE.md) and [Task Ledger](../../TASK_LEDGER.md) first. +Current reports take precedence over historical pending/passed claims. diff --git a/harness/history/2026-09-06-ledgers-before-consolidation/STATE.md b/harness/history/2026-09-06-ledgers-before-consolidation/STATE.md new file mode 100644 index 00000000..c90ebc34 --- /dev/null +++ b/harness/history/2026-09-06-ledgers-before-consolidation/STATE.md @@ -0,0 +1,207 @@ +# Current release checkpoint — 2026-09-06 19:44 PDT + +Goal remains active and incomplete. Native artifact MOVE and CREATE now pass complete source Save/one Undo/Save/one Redo/Save oracles plus controlled-stop exact draft recovery. Creation retained its reserved UUID and excluded row with eight decisions; all six saves across both cases succeeded first attempt. See release-artifacts-native-20260906.md. Current native process2540/window949683068 uses unchanged AB6ECD62 GUI and is clean on the creation result. Native deletion remains pending. Earlier process10192 and artifact-pending statements below are historical. + +Actual Linux Rust1.90.0 core-lib suite passed38/38 on Ubuntu24.04.4 QEMU; cold compile19m15s, tests14.15s, log .local/linux-qa/runtime/linux-rust190-core-lib.log. This precedes the new Linux renameat2 no-replace delta. State_contract owns one-job heavy compilation for four focused delta tests, complete library rerun and explicit strace failure injections. Windows Cargo is paused to protect host memory; an attempted semantic regression run was interrupted, not passed (.local/semantic-conflict-diagnostics-regressions.log). WSL remains unavailable; no host restart or settings changes. + +Three conflict wording tests passed before the latest reserved-label changes. Additional row-aware reserved headings/reference labels and same-slot conflict diagnostics are formatted/frozen but require tests, strict Clippy, rebuild and native verification. Behavior reviewer owns those production edits; state_representation independently found the residual conflict path and re-reviews the correction. Prior984 passing/one ignored accounting is unchanged here; do not add duplicate platform tests or unrun additions as new passes. + +Next: complete remaining native workflows, then resume Windows semantic checks/build when Linux resource lease permits; Linux publication failure tests and full application build/runtime remain gates. Update packages and fresh independent end-state review before completion. + +Historical checkpoints below retain prior evidence; this head supersedes current-status claims. +# Current release verification — 2026-09-06, evening continuation + +The authoritative objective remains attachment10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md. Full OA/SA Windows/Linux graphical/source/REST release remains incomplete. Branch codex/release1-continuation includes integrated c091f80 and local checkpoint2b2d2bc; later source6 work is uncommitted. Prior branch accounting remains in release-branch-integration-20260906.md. + +Latest tests: full desktop518/518 passed (.local/source6-artifact-inspector-desktop-tests2.log); corrected artifact6/6 passed (.local/source6-artifacts-final-tests2.log); strict all-target Clippy passed (.local/source6-artifacts-final-clippy3.log). Four additional independent artifact recovery falsifiers passed (.local/artifact-recovery-falsifier-tests2.log), and two core artifact absence oracles passed. Combined earlier full workspace and complete affected reruns now account for984 passing/one existing ignored benchmark, not one uninterrupted green full-workspace run. OpenAPI39fixtures/197schemas and HTTP48 passed. Source5 focused13 passed. Latest GUI/server build passed (.local/source6-artifacts-native-build.log), predating test-only falsifiers and subsequent diagnostic work. + +Latest native GUI is .local/native-qa/bin-source6-artifacts/arcadia-studio.exe, SHA256 AB6ECD62DB02FF3D81E2421EE1DDC926AEF72C65784DD288EAFA281836EC0597. Includes artifact lifecycle and diagram-inspector correction. Root alone operates the app; isolated appdata/system-only PATH. After a controlled stop of9060, restarted process10192 is awaiting Home Recover for an artifact move with invalid Owner '-' and separate revision Some(empty)/fragment None drafts. Checkpoint69352f76-e6ca-4e09-aaa7-7b4d94c5dccc copied .local/native-qa/artifacts-move-before-crash.json. Artifact native not yet complete. + +Verified native advances: source5 checkpoint exact old raw/baseline/include/path retention, manual draft repair and exact two-file Save (release-source5-native-recovery-20260906.md). Child/Incident two-row forced-stop recovery plus exact Apply/Save/one Undo/Save/one Redo/Save all passed, every save first attempt and recovery retired (release-attachments-native-20260906.md). Dense original10/copy9 views persisted across restart; independent exact model oracle passed. Additional coverage/context intersections, empty selections and Names-and-items preview/apply/Undo were observed; all40 label baseline/minimum-size evidence remains scoped. Latest view switch now follows original/copy inspector correctly; pending copy name applies before switch and one Undo returns clean baseline. + +Artifact owner move, independent optional values and invalid raw owner all preview correctly at1440x900 and1100x700. Keep Presence versus Owner move blocks Apply; clearing it restores valid preview. Diagnostic is too technical and same-row conflict wording is being corrected by behavior_review. Native creation/deletion/move final exact oracles remain pending. Preserved generators/fixtures are under harness/fixtures/release1-native/artifacts. + +Linux: WSL still has no working distro; earlier import failed HCS service unavailable with host reboot pending. Headless QEMU Ubuntu guest successfully booted, repaired interrupted package setup, installed GCC13.3/pkg-config and official Rust/cargo1.90; actual Rust hello compile+filesystem roundtrip passed. One-job core-lib build runs in hashed source snapshot; no Linux app build/native acceptance claim. Guest ext4 probe confirmed ordinary rename replaces empty target while renameat2 NOREPLACE preserves it; a bounded Linux publication correction is now assigned. See release-linux-validation-options.md. Host settings/restart untouched. + +Next gates: finish artifact recovery/history native; verify clearer diagnostics; CM drawing/transition and remaining selective/ordered/scenario/hosted cases; latest full checks, optimized/MSRV packages; Linux application verification and fresh cross-disciplinary end-state review. Meaningful progress continues; no test count or checkpoint completes the goal. + +Historical checkpoints below are retained; this head takes precedence for current state. +# Current release verification — 2026-09-06 + +The authoritative objective remains `C:\Users\User\.codex\attachments\10b0f701-1743-4de7-9ee1-225920e19649\goal-objective.md`. The full OA/SA Windows/Linux graphical/source/REST release goal is incomplete. Current branch is `codex/release1-continuation`; the integrated baseline and historical branch accounting below remain applicable. + +Latest automated evidence: the full current desktop suite passes **506 tests** (`.local/source6-desktop-attachments-recovery-all.log`, 97.98 s). Combined baseline and complete affected-suite reruns account for **966 passing tests and one existing ignored benchmark**; this is not a claim of one uninterrupted green full-workspace run. The prior full workspace had 938 passes and three compatibility-expectation failures; corrected release_ports (13), source_project (33) and HTTP (48) suites passed, giving 943, followed by the desktop increase from 483 to 506. HTTP 48 is included, not additive. Source-6 OpenAPI validation passes 39 fixtures / 197 component schemas. Logs: `.local/source6-workspace-tests.log`, `.local/source6-compatibility-rerun.log`, `.local/source6-http-all.log`, `.local/source6-openapi-final-check.log`. The latest root-owned strict Clippy log `.local/source6-final-clippy.log` currently reports `collapsible_if` at `semantic_review/row.rs:197`; correction/rerun and the final rebuilt binaries are pending. No agent should run Cargo concurrently with root's checks. + +Current native app is root-operated process **10236**, source-6 GUI SHA256 `5D0734D9BD6A87CCCA8FFC8C007962AEAC573ACB82E2974211FA857E93C18A72`, staged under `.local/native-qa/bin-source6` with isolated appdata, system-only PATH and adjacent libunwind.dll. It **predates Child/Incident attachment controls, source-5 checkpoint upgrade, the live-trace label correction and visible-line counts**. Passing current source tests do not update this executable. + +Source-6 native combined review passed retained invalid/excluded row editing, forced-stop checkpoint recovery, repaired two-source Apply and exact Save/Undo/Redo source oracles. Checkpoint `c33eb911-7792-4f95-ad6f-f0676a733d21` is preserved in `.local/native-qa/batch-before-crash.json`. Accepted/Redo SHA256: `D3315BBA762395205AE761E6E64029E8B4070C15A1299C6114075581DB7B0974`; Undo/pristine SHA256: `5E1F433E04621424EDAC43E53411624059E873F88DD531BFDB39093953485ED7`. All three saves succeeded first attempt, checkpoints retired and stderr was empty. This closes those observed batch/save cases, not every save contention scenario. Exact evidence is in `release-current-native-verification.md`. + +Dense native baseline is partly verified by root: at 1440×900, Fit 89% displayed all 40 labels readably without overlap; actual exchange identities 01/10/20/40 were inspected. The Commands category displayed ten lines, and one Undo restored All. At 1100×700, Fit 52% is a tiny overview; 99% working zoom plus blank-canvas pan allowed all four named exchanges/aliases to be selected without model mutation. These observations do not require presentation grouping. Dense Save/reopen, duplicate, explicit hides, labels and remaining selector cases are still pending. Automated fixture reports 151 coincident path pairs; separate label hits provided native access, so path coincidence alone is not a usability failure. + +Current additions: source-6 Communication means and explicit transition endpoints; saved Work/Participant categories and view selectors; atomic batch semantic review; presentation duplication; Child/Incident existing-record review and functional-port boundary tests; narrowly admitted source-5 checkpoint recovery. The 18-topic guide and README/scope now describe implemented behavior separately from pending acceptance. Visible counts now use the omission panel's existing candidate/filter/availability pass, with explicit Hide-count wording; no native count check is claimed. Independent source-5 review found no new material defect and is preserved in `release-source5-checkpoint-independent-review-20260906.md` (13 focused tests inspected; exact admission, raw baselines, fingerprints and cleared impact approvals). Native old-checkpoint upgrade remains pending. The Windows journal review remains in `release-journal-publication-review-20260906.md`. + +Next release gates: strict final-tree checks and rebuild; latest attachment/source-5/trace-label/count native checks; remaining CM drawing/transition and selective-view lifecycle cases; remaining ordered/scenario/fragment, artifact lifecycle and hosted variants; optimized/MSRV packages and final independent review. Linux application build/native acceptance remains required; WSL Ubuntu import previously failed with VM service unavailable and Windows reboot pending. Root owns native input and shared Cargo. No further host feature changes or repeated imports are authorized by this status update. + +The following sections retain historical checkpoints. This current head takes precedence for process state, verification totals and outstanding work. +# Current source6 integration and recovery checkpoint — 2026-09-06 + +Latest verification: full workspace run completed with 938 passing tests, three compatibility expectation failures and one existing ignored benchmark. All 483 desktop tests passed. The three failures were corrected without changing historical inputs or production behavior: explicit participant choices for the new reverse Boundary→CommunicationMean transition, and current schema targets for migration. Subsequent complete release_ports (13), source_project (33), and HTTP (48, including two new source6 success/restart cases) suites pass. Together the current test coverage is 943 passing tests and one ignored benchmark. Logs: .local/source6-workspace-tests.log, .local/source6-compatibility-rerun.log, .local/source6-http-all.log. Strict Clippy and new GUI/server builds remain pending. Normal Cargo execution of the dense-view example also passed (.local/source6-dense-example.log). Native process8908 closed cleanly after observing All changes saved; no new GUI running yet. Independent Windows journal review found no additional material defect; release-journal-publication-review-20260906.md records the bounded evidence. Agent is correcting outdated guide wording before the new build. + +Goal remains full first-release OA/SA, Windows/Linux, graphical/source/REST parity and native acceptance. Active branch codex/release1-continuation from integratedc091f80; checkpoint2b2d2bc is committed locally, not pushed. Large later source6 continuation is uncommitted. Prior branches accounted for in release-branch-integration-20260906.md; do not restore superseded investigation stash. Authoritative user objective attachment10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md. + +Current verification: first new full desktop run459pass/19semantic failures; root causes corrected. Focused82/82semantic tests now pass (.local/source6-semantic-rerun.log); app-level view-copy5/5pass (.local/source6-view-copy-app-tests.log). CM9 and source6view8pass. Latest core run39lib (including viewoptions9 and journal/race tests), Boundaryscenario3, attachmentoracle4, external sourcefiles8 allpass (.local/source6-core-recovery-tests.log). OpenAPI/schema6 closed39fixtures/197schemas pass (.local/source6-openapi-capacity-validation.log); new actualHTTP capacity tests await fullrun. None of this is new native acceptance. + +Root currently owns shared Windows Cargo: full workspace --no-fail-fast tests session89232, log .local/source6-workspace-tests.log. All existing production source frozen until run/build completed. Agents may add isolated unwired new tests/modules. Environment .local/toolchain/env.ps1, portable Rust1.98.1 gnullvm/LLVM-MinGW. Prior checkpoint869tests+strictClippy+cleanbuild do not validate new work. Need full new workspace/Clippy and GUI/server rebuild after fixes. + +Source implementation: CommunicationMean participant exchanges + explicit SA per-exchange port decisions; reverse scenario resolver including unoriented Boundary with actual lifeline/port contract and correlated Reply semantics; deliberate source/schema6 frozen1–5 admission; ExchangeCategory Work/Participant families and saved view options with explicit empty selections; cross-layer Traceability exception; shared semantic ViewIndex cached per accepted Document revision; graphical saved-view/category controls with paged searchable reference choices; atomic multi-row semantic review and recoveryv2/history pair restoration; 18-topic guide. View duplication now uses existing CreateDiagram with new view/placement identities, exact shared semantic refs/routes/options, correct state-workspace routing and stale-ID preflight. Source6 REST limits now include200 explicit communication endpoint decisions, including apply. + +Native process8908/window853042 runs .local/native-qa/bin-reference-padding/arcadia-studio.exe SHA256D402F9B09A454B7AB1239DAB9102E6E20107BCC38AFA29740B1837592E98A04E, isolatedappdata/system-onlyPATH. It precedes ALL new source6/batch/category/guide/retry changes. Current window maximized2245×1232, Traceability, saved ordered-reverse-result.arcadia, no modal/recoveryJSON. Root alone uses ComputerUse. Main ordered review crashrecovery and reverse ordered Apply/Save/Undo/Save/Redo/Save match exactsourceoracles. Referencepair gutterpasses1440×900/1100×700; othergutterfixesawaitnewbinary. Full native evidence/hashfiles in release-current-native-verification.md. + +Native reverse SaveAs and Undo Save each initially failed before journal publication, preserving model/disk, and succeeded retry. Independent publiccore reproduced18/20saves error5; slashhypothesis falsified. Fix: Windows no-replace MoveFileExW flags0 with390ms bounded5/32/33retry; journalv2 durable write-started marker avoids rolling back another completed writer when this save wrote nothing, v1recoveryretained. Allnewfalsifiers pass; post-fix30/30publicsavespass noouterretry/nojournals (.local/source-save-fixed-30-probe.log). Independent fresh review requested; footerfullerrortooltip sourceonly. + +Fixtures ready: .local/native-fixtures/semantic-batch* exact two-source120..150 range oracle, steps/hashmanifest; dense-40/dense-40.arcadia/json/oracle produced by tracked coreexample via directrustc. Dense semantics11matricespass;40routes12ms,0labeloverlaps but151coincidentpathpairs. Native readability/identityaccess still pending, so no blanketpass or groupingdecision. DirectCargoexample run stillpending afteronefixedparse-error conversion. Future grouping must remain explicitpresentation withactualmemberidentities ifnativegatefails. + +Owners: state_contract existingoa-core/source6language +sourcefilesrecovery +newHTTPsource6test; state_representation semantic_review subtree/data_fields testbridge, nowmaydraftunwiredChild/Incidentmoduleonly; behavior_review newview_options/formsview_fields/view_copy +appcopy/capacitytests andindependentjournalreview; roototherdesktop/serverintegration/ledgers/native. Coreattachment4-testoraclepassed; graphicalChild/Incident/Artifactlifecycle stillrequired, notimplemented. Source6guide content should be reviewed for newCM/viewfeatures aftercompilednativefoundation. + +Next: finish fullworkspace diagnostics, boundedfixes, Clippy andnewGUI/serverbuild; operatereal source6communication, savedcategory/context/hide/labels/duplicate, batchrecovery/history andguide atminimum/defaultsizes; actualHTTPsource6success/restart parity; densereadabilitygate; remainingorderedscenario/branch/fragmentvariants. Then completeattachment/artifactergonomics, optimized/MSRVpackages andfreshend-statereview. WSLUbuntuimportfailedHCS_E_SERVICE_NOT_AVAILABLE withVMPpendingWindowsreboot; userrestartreplypending, no repeatedhostchanges/imports. Linuxapplicationbuild/nativeacceptance remainsrequired. Meaningfulprogresscontinues; no releasecompletionclaimed. + +The following sections retain historical evidence; current process and verification status above takes precedence. + +# Current continuation — 2026-09-06 + +The full first-release goal remains active. User requested that completed Git branches be accounted for: current branch codex/release1-continuation starts from integrated c091f80; the branch audit proves meaningful side-branch work is included or patch-equivalent. No stale stash is restored. + +Current Windows evidence: 869 workspace tests pass with one existing ignored benchmark (.local/current-integrated-tests-corrected.log). Strict all-target Clippy and clean GUI/server builds pass (.local/current-integrated-clippy.log, .local/current-integrated-build-clean.log). Two equivalent guard rewrites and a test-only helper annotation closed Clippy findings after the passing test run. Shared Windows Cargo target is idle. Portable Rust1.90 GNU host plus gnullvm target is prepared; MSRV compilation is still pending. + +Native process10896 is running from .local/native-qa/bin-20260906-corrected with isolated appdata and system-only PATH. Binary660BECC8DB96B218D12805B39661B46C9F48DF4F8F07CB3B7D42C1F803BE089F includes all behavior/Home/save/layout fixes; the later clean build differs only in the stated lint/test-helper changes. Computer Use was stopped with Escape, then explicitly reauthorized by the user's @Computer message. The unfinished timed-transition checkpoint recovered exactly, including invalid empty expression. Repair30s created one Event and changed its Internal transition atomically. Save-as to state-recovered-renamed.arcadia, one Undo, Save, Redo, Save passed: Undo entry exactly pristine; Redo entry exactly accepted; checkpoints retired. Home Continue editing returns to the same Behavior state view. Minimum1100x700 navigation shows all9pages and guide Clear is no longer clipped. Root is currently on the guide; ordered and reference-pair layout native checks remain. + +REST: the staged server passed25 captured requests, twice from isolated stores, with exact five coverage warnings across source/JSON/current validation, atomic422 rejection, repaired persistence after forced restart and warning restoration by explicit replacement. Reproducible probe/fixture is tracked in harness/fixtures/rest-coverage. This is Windows service evidence, not Linux acceptance. + +Packaging: scripts stage selected dependency/runtime notices and verify source copies and every ZIP entry. QA b has528files including485licensefiles; ZIP SHA256237b30ee0c704efad89ed5c371d2c965f9e7d11ea318c97ba16ee34282456f73. Iced Icons uses its declared distributed MIT plus preserved font copyright metadata and candid provenance limits; no contrary license was established. See release-windows-package-review.md for exact clean-build hashes. Earlier QA package native clean-PATH Home/example/help/close passed; final QA b native acceptance remains. + +Linux: the QEMU pilot achieved actual Ubuntu SSH/cloud-init/filesystem/DNS validation with no failed units. Guest-only startup timeouts were adapted for slow emulation; no application build ran. User then installed WSL and authorized creating a distro. Our QEMU processes are stopped. WSL2 Ubuntu24.04 import failed HCS_E_SERVICE_NOT_AVAILABLE; VMP is enabled but Windows reports a pending reboot, with vmcompute/hns absent. User was asked to restart when convenient. No existing distro was touched and no Windows features/settings were changed by the agent. Verified official image and exact isolated retry command are in release-linux-validation-options.md. Recheck after restart before concluding nested virtualization is unavailable. + +Required next work: ordered/keyed/native view checks and source-oracle capture; checkpoint verified work in Git; implement first-class OA CommunicationMean plus deliberate source6 admission, saved selective/category/context views, remaining attached-child/incident/artifact/multi-source review GUI, full engineering guide, MSRV/optimized packages and fresh Windows/Linux end-state acceptance. Selective-view candidate and independent critique now define family-specific category sets, global versus selected-context coverage, preserved port geometry and a dense40-edge readability gate. Nothing here narrows the original goal or marks the release complete. + +The material below records earlier checkpoints. This current section takes precedence for process state, tool ownership and current evidence. +Authoritative objective: attachment `10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md`, read in full. User requested accounting for other Git branches. Current working branch is `codex/release1-continuation` from integrated `c091f80`; all meaningful side-branch production work is present or patch-equivalent. See release-branch-integration-20260906.md. Original checkout `ea2e8e9` was stale; the duplicate investigation is stashed and superseded. + +Historical paths/windows/builds below are evidence from the previous environment, not current active processes. This machine has no D:/CodexBuilds or installed WSL. The first release is incomplete. + +Current implementation: Home/recents, semantic Annotation/Binding review, private accepted-source/raw-draft checkpoints including workflow recovery, and typed StateAction detail rows. Independent reviews found and closed checkpoint late-write/discard races, candidate-parent picker context and literal-UUID expression rendering defects. Native acceptance is still required. + +Current toolchain: user-local Rust 1.98.1 `stable-x86_64-pc-windows-gnullvm` plus official LLVM-MinGW 20260826 UCRT under ignored `.local/toolchain`; dot-source `.local/toolchain/env.ps1`. GNU test executables with installer-like filenames initially failed Windows error 740. Shared build scripts now embed an explicit `asInvoker` manifest in GNU/gnullvm binaries and tests, without changing system security or requiring elevation. + +Fresh workspace tests pass **840 tests** with one existing benchmark ignored; `.local/current-workspace-tests-final.log`. Strict all-target workspace Clippy passed in `.local/current-clippy.log`. Initial keyed fixture, core row-order and state test failures are fixed; a hosted timeout test's invalid admission-timing assumption is also corrected. Independent review fixed write/restore checkpoint workspace limits before a prior valid checkpoint can be replaced. + +Current native Windows evidence is in `release-current-native-verification.md`: empty Home/default/minimum layout, example, rejected raw source crash/recover, exact21-file Save as, recovery retirement, Recents reopen, rejected raw UUID crash/recover and guarded Save & continue passed. All three typed StateAction rows select the correct relation/phase/target in Behavior. Native found a saved-state-view entry bug: Architecture uses a generic renderer and wrong tools. Routing fix passed435 desktop tests and GUI/server build. Updated native process796 is running; invalid Binding crash recovery, exact repaired Save and one-step Undo/Redo passed. Annotation workflow recovered and saved exactly against the oracle; native found Save-as basename rename adds an unwanted Undo step and can clear Redo. A bounded history-preserving correction is being implemented. + +Root controls native input. Semantic worker currently owns shared Cargo for ordered-review focused tests; coverage diagnostics follow. Save-as history worker must coordinate build access. All native trial data lives under ignored `.local/native-qa`, with app metadata redirected away from the user's real recents. Current uncommitted continuation still needs native semantic/binding/history and behavior/workflow acceptance, fresh routing checks and a milestone commit. It is not a complete release. + +The current coverage audit is `release-current-coverage-20260906.md`. Independent OA design/critique agrees that ordinary participant communication needs a first-class CommunicationMean instead of broadening SA port-bound BoundaryExchange. Other remaining release work includes saved selective/coverage views, completeness diagnostics, ordered/attachment/artifact/multi-source review controls and final platform/package gates. These are required unfinished work, not exclusions. + +--- +# Current release state — 2026-09-06 + +The first-release goal remains active and incomplete. Authoritative objective: +attachment10134df9-bea2-4c02-a820-67b53d6b86f7/goal-objective.md. PLAN/TASK_LEDGER +retain complete OA/SA, graphical/source/includes/REST parity and release acceptance. + +## Ownership and integrated state + +- Main C:/Users/sorok/OneDrive/Documents/ChatGPT/Capella 2 is the continuation + pointer, codex/release1-engineering,4fa9591; production work is in DATA below. +- DATA D:/CodexBuilds/Capella2-data-integration, codex/data-contract-integration, + production14b8e18. Root owns index and Capella2-data-target (idle). +- Linux D:/CodexBuilds/Capella2-data-linux-review, codex/data-linux-review, + equivalent9c61d0d. Root owns index and data-linux-target (idle). +- SEM D:/CodexBuilds/Capella2-semantic-review, codex/semantic-review,6fb01ae. + semantic_gui_design owns slice2 Annotation/Binding GUI, index and + semantic-desktop-target. Slice1 exact reset/unordered Member/Literal is integrated + and independently reviewed; ordered/attachment/artifact/multi-source work remains. +- HOST D:/CodexBuilds/Capella2-hosted-desktop, codex/hosted-desktop,e807086. + semantic_integration owns Home/start/recents, index and scenario-target. Child + owns new Home tests; no overlap with root or SEM. Recovery is not implemented. +- Root is sole native operator. Agents may not issue native input. + +## Verified integration + +R6 data contracts, direct scenario/process gestures, source5 core/REST and scalar/ +constructor GUI are integrated. Full Windows767/Linux770 workspace tests passed +at DATA28ebbbb/Linux04210b3, one intentional benchmark ignore each; strict checks +and optimized desktop/server builds passed. Live REST184 schemas/19 typed values/ +9 malformed rejections passed. See scoped ledgers for limits. + +Newest DATA14b8e18/Linux9c61d0d:392 desktop tests each, strict all-target desktop +Clippy, workspace fmt and release builds pass. Sessions24877/10919 closed0. +Logs integrated-review-{test,clippy,build}.log in Windows data-target and Linux +checkout target. Initial Windows log redirect failed before Cargo (nonexistent +checkout target); rerun used the existing dedicated target and passed. + +Independent reviews closed constructorc149f2f (299 tests), reset/unordered6fb01ae +(311 tests), invalid-local-source destinationb04c4b3 and catalog12e3020/e807086. +Catalog review found and corrected false empty filtered results when Check observes +a changed head outside displayed rows; unchanged-head/cross-service controls pass. +Constructor and keyed core changes43370de/8a75294 have independent probes and359 +core tests passing. Full graphical parity is not yet established. + +## Native state and evidence + +Windows hosted6fcc238 trial71940 closed0/empty stderr. Default/minimum publish, +graph/text Save, invalid source retention, artifact API Check/review/Reload/Undo, +stale ETag conflict/local retention, offline/retry, exact three-file local Save As +and catalog Open passed. Isolated loopback serverPID20984 remains at127.0.0.1:8787, +resource d28b2fc9-fbbd-456b-8ba8-a1195d1c8f33 revision6. Evidence/store is +C:/Users/sorok/AppData/Local/Temp/capella2-hosted-native-20260906. + +Invalid local repair source used to retain hosted destination and overwrite that +resource after repair. Fix is integrated/reviewed; actual fixed native flow remains +pending. Catalog freshness is likewise integrated/reviewed, native check pending. + +Current Windows constructor trial84997/window4327540 uses frozen cf04ff1 at +ui-constructors-windows-cf04ff1, SHA86C86A0A4E9E7BDEB5BBB64F6787AA5D3A8715D109979A57D056AEB686538E38. +At1100x700, fixture01 Data reviewed Take, exact source/history, Undo/Redo and Save +passed; parsedProject matched independently core-generated expected. Candidate +Real retained raw '-' across Integer/Real changes, blocked incompatible Boolean +default, accepted repaired -1.5..100/default3.5, applied as one Undo/Redo and saved. +Current accepted file is fixtures/01-data-switch.arcadia with Real candidate; +native property values/history and disk comments/unitcm confirmed. Independent +candidate exactProject comparison is pending. No modal currently open. + +Remaining five pristine constructor fixtures and expected outputs are in +C:/Users/sorok/AppData/Local/Temp/capella2-constructor-native-20260906. Do not rerun +the UUID-generating fixture generator. Copy originals before editing. + +New frozen accepted builds (native pending) under Capella2-release1-01a06e2e: +ui-integrated-review-windows-14b8e18 and ui-integrated-review-linux-9c61d0d. +Manifests contain hashes. Earlier scalar native accepted; process camera/owner +accepted, compact pending form fixf08507a needs native recheck. See native ledgers. + +Linux uses Ubuntu-24.04, Rust1.98.1 PATH/home/sorokinoleg/.cache/arcadia/rust/bin, +--config target/linux-cargo.toml and dedicated target. Wayland works with +WGPU_BACKEND=vulkan and VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json. +Optional X11 missing libxkbcommon-x11-0; user has sudo apt-get install command. +Do not bypass authentication. Windows/Wayland/REST are unblocked. + +## Exact next actions + +1. Complete native constructor fixtures and compact process pane acceptance. +2. Operate corrected invalid-local-open and stale catalog on newest Windows build; + complete Linux hosted/semantic native acceptance. +3. Independently review/integrate SEM Annotation/Binding and HOST Home/recents, + then ordered/attachment/artifact/multi-source GUI and recoverable checkpoints. +4. Finish advanced OA/process/scenario, help/coverage and fresh final reviews; + full platforms/MSRV/docs/package verification. No release handoff yet. + +Build: cargo test --locked --offline --workspace; +cargo clippy --locked --offline --workspace --all-targets -- -D warnings; +cargo build --locked --offline --release -p oa-desktop -p oa-server. +Dedicated targets only. No remote push, usage reset or broad target deletion. + +## Latest integration evidence (after the active-section snapshot) + +Desktop test executable built. Full desktop run:459pass/19semantic failures (.local/source6-desktop-tests.log). Constructors' all-NotApplicable retired-slot handling, first-equal Candidate payload preservation, exact-source test fixture, stale wording and one approved stale/reset expectation were corrected by semantic owner. Focused82semantic tests currently running root63955 (.local/source6-semantic-rerun.log). Five app-level duplication tests now wired; missing-view preflight before Apply fixed an independently found unrelated-draft commit. Copy helper3, selective forms2, cache invalidation, CM GUI and SVG tests all passed in459; new app-level copy tests not run yet. + +Core focused:CM9 and source6/view8 pass (.local/source6-core-focused2.log). Boundary scenario2/3pass; missing explicit Name KeepTarget fixture decisions corrected. Attachment oracle4failed invalid Boolean-vs-Record association fixture; now corrected, rerun pending. OpenAPI/schema6 generator validation37fixtures/197schemas passes (.local/source6-openapi-validation.log); actual HTTP new contracts pending. Dense generator compile error corrected; not executed yet. + +Native reverse ordered Apply/Save/Undo/Save/Redo/Save matches exact source oracles. Two Save attempts failed with transient Windows journal-publication access denied; each retry succeeded without reauthoring/model loss. Core independently reproduced18/20; bounded390ms Windows publication retry with3tests nowsource-only. Root fullerrorfooter tooltip source-only. Detailed native hashes in release-current-native-verification.md. Current process8908/window853042 maximized, saved ordered-reverse-result.arcadia, no modal or recovery checkpoints. Oldbinarystillprecedesallnewwork. RootownsCargo; existingcore/source6productionfrozen duringfocuseddesktoprun. Next run correctedcorefixtures, journalretrytests/30saveprobe, densefixture, appcopytests, fullworkspace and rebuiltnativeacceptance. Goal remains incomplete. diff --git a/harness/history/2026-09-06-ledgers-before-consolidation/TASK_LEDGER.md b/harness/history/2026-09-06-ledgers-before-consolidation/TASK_LEDGER.md new file mode 100644 index 00000000..b3559130 --- /dev/null +++ b/harness/history/2026-09-06-ledgers-before-consolidation/TASK_LEDGER.md @@ -0,0 +1,192 @@ +# Current task facts — 2026-09-06 19:44 PDT + +Verified: artifact MOVE and CREATE native exact Save/Undo/Redo plus raw/excluded-reserved recovery pass; current root-operated process2540 is clean. Linux Rust1.90 core-lib38/38 passed before Linux publication delta. Evidence and scope are in current STATE.md and release-artifacts-native-20260906.md. + +Open: native deletion/CM/remaining scenario and hosted cases, reserved-label correction acceptance, latest complete checks and optimized packages, Linux journal delta/full application/runtime, fresh independent end-state review. Windows semantic regression was deliberately interrupted for host memory; it is incomplete. Linux worker retains heavy compile lease. + +Decision/strategy: preserve exact oracle comparisons and explicit draft decisions; row-aware labels must distinguish reservations from deleted records. Continue authorized engineer workflows while platform verification proceeds. Goal and original requirements remain unchanged and incomplete. + +Historical ledger below retains prior rationale; current head supersedes old status. +# Task Ledger — current facts, 2026-09-06 evening + +Goal and given requirements remain full first-release OA/SA, equivalent graphical/source/REST, Windows/Linux native usability, strict reST and independent acceptance. Authoritative objective attachment10b0f701-1743-4de7-9ee1-225920e19649 remains active and incomplete; branch integration is preserved. + +Verified facts: desktop518 full plus four recovery falsifiers passed; combined baseline/component accounting984 passing and one ignored (not uninterrupted full-workspace green). Strict all-target Clippy and rebuilt GUI/server passed before later diagnostic changes. Source5 recovery and Child/Incident forced-stop/recovery/exact Save/Undo/Redo native pass. Dense view copy/hide persistence and independent exact model oracle pass; view-inspector switching fix observed. See current STATE.md and specific native reports for exact limits. + +Current strategy: complete artifact native recovery and exact history; improve conflict explanations using actual facet/owner names; close remaining engineer workflows, then final checks/package/Linux acceptance and fresh independent review. Native process10192 awaits artifact Home Recover; root owns all native input. Implementation agents own only assigned files. Avoid concurrent Windows Cargo builds. + +Open questions/assumptions: Linux full application build/runtime remains unverified. QEMU guest toolchain and hello are verified, core-lib build is active; WSL remains unavailable pending host reboot. A measured Linux empty-target journal publication race requires a no-replace correction. Artifact native creation/deletion/move, CM transition controls and remaining scenario/hosted variants still need actual execution. Passing unit tests are not substitutes for these workflows. + +Decision: preserve raw invalid/rejected drafts through recovery, use exact identities and explicit candidate decisions, and apply model/source/history atomically. Native evidence supports child/incident and batch cases; artifact case is in progress. Do not silently broaden scoped evidence or weaken release completion. + +Historical ledger below remains for prior rationale and evidence; this head and current STATE.md take precedence. +# Task Ledger — current release head, 2026-09-06 + +Authoritative objective: `C:\Users\User\.codex\attachments\10b0f701-1743-4de7-9ee1-225920e19649\goal-objective.md`. Goal remains incomplete: full OA/SA engineer workflows and graphical/source/REST parity, Windows/Linux native acceptance, reproducible packaging and independent final review. Earlier objective IDs and status statements below are historical; this current head and STATE.md take precedence. + +## Current verified facts + +- Current desktop suite: **506 passed** in `.local/source6-desktop-attachments-recovery-all.log`. Combined baseline and complete corrected component-suite coverage: **966 passing / one existing ignored benchmark**, including HTTP 48; not one uninterrupted full-workspace green run. OpenAPI: 39 source-6 fixtures, 197 schemas. STATE.md records the accounting and exact logs. +- Source-6 native batch: included Minimum5 and excluded invalid Maximum `-` survived a forced-stop checkpoint and Home Recover. Repair to Minimum120/Maximum150 applied once. Save/Redo exactly match SHA256 `D3315BBA762395205AE761E6E64029E8B4070C15A1299C6114075581DB7B0974`; one Undo/Save exactly matches `5E1F433E04621424EDAC43E53411624059E873F88DD531BFDB39093953485ED7`. All three saves succeeded first attempt, with retired checkpoints and empty stderr. See `release-current-native-verification.md`. +- Dense partial native result: 1440×900 Fit89% shows all forty labels readably, identities01/10/20/40 confirmed; Commands filter10 and one Undo→All pass. At1100×700 Fit52% is an overview, while99% plus pan provides readable access to all four names/aliases without a model edit. No grouping requirement follows from these observations; dense persistence, duplication, hides and remaining filter/label cases remain pending. +- Independent source-5 checkpoint review found no new material defect in exact token-only migration, raw baseline preservation, historical fingerprint binding, replan/approval reset, bounds or restore-before-reset handling. Evidence: `release-source5-checkpoint-independent-review-20260906.md`; 13 focused tests inspected. Native upgrade is not yet verified. + +## Current boundaries and next actions + +Root's native process10236 runs source-6 binary SHA256 `5D0734D9BD6A87CCCA8FFC8C007962AEAC573ACB82E2974211FA857E93C18A72`; it predates attachment controls, source-5 checkpoint recovery, the trace-label correction and visible counts. Root owns native input and shared Cargo. `.local/source6-final-clippy.log` currently reports one collapsible-if finding in semantic_review/row.rs; correction/rerun and final rebuilt binaries remain pending. + +Communication means, selective views, duplication and batch review are implemented. Existing-record Child/Incident review now has automated evidence; its native acceptance and remaining Artifact/lifecycle ergonomics are still required. Finish new-binary recovery/attachment/count checks and remaining CM/selective/ordered/hosted cases, then optimized/MSRV builds, packages/checksums and full independent end-state review. Linux application build/native acceptance is still required; WSL VM service remains blocked pending the previously requested host reboot. No checkpoint or test count completes the user goal. + +## Historical ledger retained below +# Task Ledger — first release + +Authoritative objective: attachment10134df9-bea2-4c02-a820-67b53d6b86f7/ +goal-objective.md, read fully2026-09-06. STATE.md records execution; PLAN.md retains +scope; DECISIONS.md and linked contracts preserve rationale. + +## Goal and success criteria + +System engineers develop and iteratively reconcile OA/SA models through equivalent +graphical, textual and REST workflows. Rust/Iced Windows/Linux, strict reST, +traceability, navigable offline guidance and reproducible deployment. Acceptance +requires native workflows, semantic evaluators and fresh independent review. + +## Given facts + +- User prioritizes Capella domain fidelity, hierarchy, typed ports and intuitive + transitions in both directions, without its clutter and navigation friction. +- Direct exchange drawing must avoid large endpoint dropdowns; ports must move + between sides; contextual menus should replace persistent action clutter. +- Modular source must be practical for humans/AI and Git. Text/graph/API parity is + required. Behavior canvases must support editing and preserve drag selection. +- Git/worktrees are authorized. Remote publication is not requested. + +## Verified facts + +- Original Capella SAB creates FE function input/output endpoints; component ports + are separately Standard/Flow with explicit allocation. Native observations are in + release-capella-workflow-benchmark.md; adopted contracts preserve that distinction. +- Explorer/port discovery, editable routing, nested modular source and coupled REST + bundles have scoped Windows/Linux acceptance. Evidence links are in STATE.md. +- Direct palette connections, four-side port anchors, context menus and source + folders passed real Windows/Linux operation. Passive stacked Canvas fixes native + tooltip overprint; separate Frames did not. See release-port-drawing.md. +- Persisted state editing now passes Windows/Linux native and independent review: + creation, movement, direct transitions, typed forms, reconnection, fold/reopen. + See release-state-editing.md. Scenario/process canvases still need direct editing. +- Ordinary Document commands atomically synchronize source/model; failed writes + retain drafts. Source comments require occurrence/provenance-aware edits, not + whole-model equality or flat reformatting. Independent counterexamples were fixed. +- Accepted-main R3 existing-target reconciliation remains name/description only; + isolated typed core/source/API are implemented, with desktop integration active. R6 data shape + expansion is active implementation, not accepted functionality. +- R6 ab661f2 full suites passed Windows607/Linux610; f5467e5 desktop263 and + strict Clippy/release passed on both platforms. Native Windows data editing, + typed defaults, allocations, recursive qualified Call/Reply values, history and + saved reopening passed. Source3 migration now preserves comments and nesting. + Native found flat property browsing still ambiguous; one-record browsing with + bounded pages passes264 desktop tests/Clippy/release on both platforms and native + Windows compact/normal browsing, expression retention and saved reopen. + Linux native R6 acceptance remains outstanding. + +## Assumptions and open questions + +- Nested aggregate files reduce cognitive load; any additional port/exchange/step + shorthand must justify grammar complexity through real authoring tasks. +- State placements proved viable without a second geometry system. Scenario/chain + gestures still need their own occurrence/order semantics and task verification. +- Routing offers a usable initial path plus manual pins; arbitrary dense diagrams + are not guaranteed crossing-free. Measured dense full solve119ms remains a limit. +- R3 selected design C uses finite self-tagged typed facets, separate membership + and order acknowledgements, and transient constructor groups. Source5 historical + references are implemented in the isolated semantic checkout. Independent review + found narrow-acknowledgement/canonical-agreement/rollup defects; the worker has + added regressions and fixes. Full catalog, GUI/API/spec and acceptance remain. + +## Decisions and current strategy + +Preserve one typed core and validated command boundary. Retain current typed ports; +R6 uses schema/source4 with explicit1/2/3 migration and no intermediate public4 +shape. Freeze payload, defaults, inheritance, associations and identified interface +allocations together. Preserve lexical includes/comments during migration. + +Finish and independently verify R6, then semantic R3 reconciliation and direct +scenario/process gestures. Complete lifecycle/recovery, hosted editing, guidance +and fresh cross-disciplinary Windows/Linux acceptance. No subsystem checkpoint +completes the first release. Current work is making measured implementation, +review and runtime progress. User reauthorized Computer Use. Root resumed combined Windows scenario tests; +middle insertion, history, save, reorder and scroll/Escape cancellation passed. +Three native UI findings are being refined; Linux acceptance remains pending. + + +Latest verified facts: integrated60d162c passes306 desktop tests/strict Clippy/build +on Windows and Linux. Windows ALT/pinned-actions/viewport retention native passed; +Linux native is active. RESTd130013 passed48 server tests plus served/local schema +validation and independent review after direct user confirmation resolved approval. +New constructor-acknowledgment reachability defect was independently reproduced; +narrow core fix and separate scalar GUI implementation are active in SEM. These +are current unresolved work, not accepted release limitations. See current STATE. + +## Current evidence superseding earlier in-progress notes — 2026-09-06 + +Semantic source5/core/REST and scalar GUI are integrated at d114a5d. Full Windows718/ +Linux721 tests, strict Clippy, optimized builds and live semantic REST contracts pass. +The equal initial discriminator acknowledgment defect is fixed and independently +closed. Three scalar GUI P2 fixes5a51a75 are under fresh recheck/integration; structured +authoring remains required. Basic Linux R6, middle scenario and guarded chain insertion +have actual native evidence in their ledgers; advanced and compact cases remain. +X11 lacks libxkbcommon-x11-0, installation requires user's sudo password. User has +the exact fix command; Windows/Wayland progress is independent. This environment +issue is not evidence of a semantic defect or reason to mark the release blocked. + +2026-09-06: DATA67ed8e0/Linux43dd566 integrates bounded MessageKind/TimerExpression +review.335 desktop tests and strict checks/build pass per platform; independent22 +module tests+288 differential cases close no findings. Windows native scalar now +includes named mapping/default, historical identities, reopen and reverse Undo; +trial closed0/empty stderr. Exact scope and pending native cases: +release-semantic-native.md. Hosted desktop implementation is isolated in +Capella2-hosted-desktop; all release completion claims remain pending. + +## Hosted/native and constructor checkpoint — 2026-09-06 + +Verified: optional hosted desktop is integrated; native publish, exact authored +bundle preservation, graphical/text Save, invalid-source rejection, API artifact +review/reload/Undo/Redo and competing-revision local retention passed on Windows. +Minimum hosted dialog controls remain reachable. Offline/reopen/local Save As and +Linux native hosted acceptance continue. No external server was deployed. + +Independent core review closed43370de/8a75294 with31 semantic tests, optional +Binding/Annotation both-direction probes,3 reverse Port boundary and13 port +regressions. Full owner core suite359 passes. Semantics preserve target-local keyed +members without resurrecting source removals, mandatory reset and OA Legacy rules. +Remaining constructor GUIc149f2f passes299 SEM tests; fresh review is active. + +Native process camera/owner fix acceptance passed. New compact form finding was +fixed and passed real Iced layout+360 desktop tests; native fix acceptance pending. +Strategy remains measured release completion, not treating these slices as full +source/UI/API parity. Keyed/order/reset, artifacts/multi-source, remaining advanced +native cases, Home/recovery/help/coverage and final acceptance are still required. + +## Reviewed corrections and native Data evidence — 2026-09-06 + +Verified: constructorc149f2f/reset-unordered6fb01ae and hostedb04c4b3/cataloge807086 +are independently reviewed/integrated. Both platforms pass392 desktop tests and +strict gates. Native Data Take matched the whole core-generated Project/history; +Candidate raw invalid/type mismatch/repair/compact Apply/Undo/Redo/Save passed. +Candidate exactProject and remaining constructors/Linux native are still pending. +Strategy: finish structured parity while Home/recents progresses independently; +recovery, ordered/attachments/artifacts/multi-source and final acceptance remain. + + +## 2026-09-06 branch reconciliation +Verified integrated baseline c091f80 contains direct state/scenario/process editing, data contracts, schema 5 semantic review and hosted editing. Current branch codex/release1-continuation adopts it; independent ancestry/patch/tree audits found no required production transplant. Historical environment is absent here. Current user objective is attachment 10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md. See release-branch-integration-20260906.md for evidence and ownership. Assumption: portable user-local GNU tooling can verify Windows without system installation; being tested, not yet established. + +## Current source6 integration — 2026-09-06 + +Verified facts: portable user-local GNU/LLVM Windows build and native app work without a system toolchain installation; checkpoint2b2d2bc has869passing workspace tests, strict Clippy and clean builds. Exact-source native ordered review and independent timed multi-file recovery oracles pass. The side-branch audit establishes current integrated ancestry. These results do not validate later uncommitted source6 work. + +Given facts: user installed WSL and permits an isolated Linux distro. WSL import currently fails because its VM service is unavailable with a pending Windows reboot. User restart reply is pending; Linux application acceptance remains required. + +Current design: participant CommunicationMean, deliberately frozen old1–5 admission and source6 additions; saved view category/context/coverage/edge/label selectors; explicit both-layer Traceability exception; exact atomic multi-row semantic review and recoverable raw drafts. Decisions and rationale are in DECISIONS.md. Existing source changes are undergoing integration, not claimed as accepted features. + +Open questions/evaluators: dense forty-exchange readability and access to every actual relation; remaining child/incident/artifact review ergonomics; complete source/REST/OpenAPI parity; source6 native lifecycle at minimum/default sizes; optimized/MSRV/Windows/Linux end-state acceptance. Presentation duplication is a bounded next step using existing CreateDiagram, preserving semantic identities. Current strategy is compile/test decisive cases before expanding UI work, then real native evaluation and independent review. \ No newline at end of file diff --git a/harness/native-release1-review-check.rs b/harness/native-release1-review-check.rs new file mode 100644 index 00000000..0a2706b4 --- /dev/null +++ b/harness/native-release1-review-check.rs @@ -0,0 +1,107 @@ +//! Independent exact native lesson oracle, compiled against existing core libraries. +//! Usage: CHECKER oa|review NATIVE_ENTRY BEFORE_REVIEW_ENTRY OUTPUT_DIRECTORY +#[path = "../examples/release1/model.rs"] +mod teaching; +use oa_core::{source_files, source_project::*, transitions::*, *}; +use std::{fs, path::Path}; + +fn disk(entry: &Path) -> CompiledSourceProject { + let read = source_files::load_project(entry, ProjectLimits::default()).unwrap(); + assert!( + read.diagnostics + .iter() + .all(|diagnostic| diagnostic.diagnostic.severity != validation::Severity::Error), + "{:?}", + read.diagnostics + ); + compile_bundle(&read.reachable_bundle(), ProjectLimits::default()).unwrap() +} +fn compare(expected: &CompiledSourceProject, actual: &CompiledSourceProject, label: &str) { + assert_eq!( + expected.decoded.project, actual.decoded.project, + "{label}: full model differs; no metadata/identity exclusions are allowed" + ); + assert_eq!( + expected.bundle.entry, actual.bundle.entry, + "{label}: entry differs" + ); + assert_eq!( + expected.bundle.files.keys().collect::>(), + actual.bundle.files.keys().collect::>(), + "{label}: module set differs" + ); + for (name, text) in &expected.bundle.files { + assert_eq!( + text, &actual.bundle.files[name], + "{label}: exact source differs in {name}" + ); + } +} +fn main() { + let args: Vec<_> = std::env::args().collect(); + assert_eq!( + args.len(), + 5, + "usage: CHECKER oa|review NATIVE_ENTRY BEFORE_REVIEW_ENTRY OUTPUT_DIRECTORY" + ); + assert!(matches!(args[1].as_str(), "oa" | "review")); + let lesson = teaching::build(); + assert_eq!(lesson.before_transition.id, lesson.project.id); + assert!(!lesson.request.sources.is_empty()); + let original = compile_bundle( + &teaching::checked_bundle(&lesson.project), + ProjectLimits::default(), + ) + .unwrap(); + let source = teaching::id("oa_confirm"); + let target = teaching::id("sa_confirm"); + let mut renamed = lesson.project.elements[&source].clone(); + renamed.name = "Confirm stable pressure".into(); + let renamed_model = + execute_batch(&lesson.project, &[Command::EditElement(source, renamed)]).unwrap(); + let before = update_bundle(&original, &renamed_model, ProjectLimits::default()).unwrap(); + compare( + &before, + &disk(Path::new(&args[3])), + "preserved OA rename baseline", + ); + let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![source]); + transition.fields.clear(); + transition.mappings.push(TransitionMapping { + source, + target: Some(target), + target_kind: None, + }); + let mut request = SemanticTransitionRequest::new(transition); + request.reviews.push(SemanticReviewRequest { + source, + resolutions: vec![FacetResolution { + facet: FacetSelector::Header(HeaderFacet::Name), + decision: SemanticDecision::TakeSource, + }], + reset: vec![], + }); + let plan = plan_semantic_transition(&before.decoded.project, request).unwrap(); + assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); + let reviewed = apply_semantic_transition(&before.decoded.project, &plan).unwrap(); + let after = update_bundle(&before, &reviewed, ProjectLimits::default()).unwrap(); + let expected = if args[1] == "oa" { &before } else { &after }; + let output = Path::new(&args[4]); + fs::create_dir_all(output).unwrap(); + fs::write( + output.join("expected-model.json"), + serde_json::to_string_pretty(&expected.decoded.project).unwrap(), + ) + .unwrap(); + fs::write( + output.join("expected-source-bundle.json"), + serde_json::to_string_pretty(&expected.bundle).unwrap(), + ) + .unwrap(); + compare(expected, &disk(Path::new(&args[2])), "native result"); + println!( + "PASS {}: exact full model and {} source files; preserved OA rename baseline verified; no identity or metadata normalization", + args[1], + expected.bundle.files.len() + ); +} diff --git a/harness/probes/chain_context_resolution.rs b/harness/probes/chain_context_resolution.rs new file mode 100644 index 00000000..8d7cd867 --- /dev/null +++ b/harness/probes/chain_context_resolution.rs @@ -0,0 +1,437 @@ +//! Design probe only: no production schema changes, no execution semantics. +//! Compile directly against an existing oa_core rlib; see the adjacent design. +use oa_core::*; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct Endpoint { + references: Vec, + occurrence: Id, +} +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct Address { + owner: Id, + endpoint: Endpoint, +} +#[derive(Clone, Debug, PartialEq, Eq)] +struct Resolved { + address: Address, + work: Option, +} +#[derive(Clone, Debug)] +enum Purpose { + Precedence { guard: String }, + Exchange { exchange: Id }, + CommonWork { work: Id }, +} + +struct Record { + parent: Option, + layer: Layer, + kind: Kind, + reference: Option, + work: Option, + contribution: bool, +} +struct Index<'a> { + project: &'a Project, + records: BTreeMap, + cache: BTreeMap, + max_depth: usize, + cache_capacity: usize, +} +impl<'a> Index<'a> { + fn new(project: &'a Project, max_depth: usize, cache_capacity: usize) -> Self { + let records = project + .elements + .values() + .map(|e| { + let (reference, work, contribution) = match e.shared() { + Some(SharedBody::Occurrence { body, .. }) => match body { + OccurrenceBody::SequenceReference { sequence } => (*sequence, None, true), + OccurrenceBody::Action { action, .. } => (None, *action, true), + OccurrenceBody::Exchange { .. } | OccurrenceBody::Control { .. } => { + (None, None, true) + } + _ => (None, None, false), + }, + _ => (None, None, false), + }; + ( + e.id, + Record { + parent: e.parent, + layer: e.layer(), + kind: e.kind(), + reference, + work, + contribution, + }, + ) + }) + .collect(); + Self { + project, + records, + cache: BTreeMap::new(), + max_depth, + cache_capacity, + } + } + fn resolve(&mut self, owner: Id, endpoint: &Endpoint) -> Result { + if endpoint.references.len() > self.max_depth { + return Err("reference depth limit"); + } + let address = Address { + owner, + endpoint: endpoint.clone(), + }; + if let Some(resolved) = self.cache.get(&address) { + return Ok(resolved.clone()); + } + let root = self.records.get(&owner).ok_or("missing owner")?; + if !matches!(root.kind, Kind::Process | Kind::FunctionalChain) { + return Err("wrong owner kind"); + } + let mut current = owner; + let mut visited = BTreeSet::from([owner]); + for step in &endpoint.references { + let reference = self.records.get(step).ok_or("missing path component")?; + if reference.parent != Some(current) || reference.layer != root.layer { + return Err("path is not direct in this context"); + } + let next = reference + .reference + .ok_or("path component is not a resolved sequence reference")?; + let target = self + .records + .get(&next) + .ok_or("missing referenced sequence")?; + if target.layer != root.layer || target.kind != root.kind { + return Err("incompatible referenced sequence"); + } + if !visited.insert(next) { + return Err("recursive sequence reuse"); + } + current = next; + } + let leaf = self + .records + .get(&endpoint.occurrence) + .ok_or("missing leaf")?; + if leaf.parent != Some(current) || leaf.layer != root.layer || !leaf.contribution { + return Err("leaf is not a direct contribution of the resolved owner"); + } + let resolved = Resolved { + address: address.clone(), + work: leaf.work, + }; + // Cache residency cannot change the validity of an authored address. + // The production implementation can use LRU; deterministic eviction is + // enough to falsify the earlier history-dependent admission mistake. + if self.cache_capacity > 0 { + if self.cache.len() >= self.cache_capacity { + self.cache.pop_first(); + } + self.cache.insert(address, resolved.clone()); + } + Ok(resolved) + } + fn resolve_many( + &mut self, + owner: Id, + endpoints: &[Endpoint], + max_unique: usize, + ) -> Result, &'static str> { + let unique: BTreeSet<_> = endpoints.iter().collect(); + if unique.len() > max_unique { + return Err("analysis address limit"); + } + endpoints + .iter() + .map(|endpoint| self.resolve(owner, endpoint)) + .collect() + } + fn connection( + &mut self, + owner: Id, + source: &Endpoint, + target: &Endpoint, + purpose: &Purpose, + ) -> Result<(), &'static str> { + let a = self.resolve(owner, source)?; + let b = self.resolve(owner, target)?; + if a.address == b.address { + return Err("same contextual occurrence"); + } + match purpose { + Purpose::Precedence { guard } => { + let _opaque_guard = guard; + Ok(()) + } + Purpose::CommonWork { work } if a.work == Some(*work) && b.work == Some(*work) => { + Ok(()) + } + Purpose::CommonWork { .. } => Err("common work does not match both contributions"), + Purpose::Exchange { exchange } => { + let relation = self + .project + .relations + .get(exchange) + .ok_or("missing exchange")?; + if !matches!( + relation.kind(), + RelationKind::Interaction | RelationKind::FunctionalExchange + ) || a.work != Some(relation.source()) + || b.work != Some(relation.target()) + { + return Err("exchange endpoints do not match contextual work"); + } + Ok(()) + } + } + } +} +fn id(value: u128) -> Id { + Id::from_u128(value) +} +fn element(p: &mut Project, n: u128, kind: Kind, layer: Layer, parent: Option) -> Id { + let mut e = p + .make_element(kind, layer, &format!("item_{n}"), parent) + .unwrap(); + e.id = id(n); + p.elements.insert(e.id, e); + id(n) +} +fn occurrence(p: &mut Project, n: u128, layer: Layer, owner: Id, body: OccurrenceBody) -> Id { + let key = element(p, n, Kind::Occurrence, layer, Some(owner)); + *p.elements.get_mut(&key).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { + body, + precondition: String::new(), + postcondition: String::new(), + }; + p.elements + .get_mut(&owner) + .unwrap() + .steps_mut() + .unwrap() + .push(key); + key +} +fn endpoint(path: &[u128], leaf: u128) -> Endpoint { + Endpoint { + references: path.iter().copied().map(id).collect(), + occurrence: id(leaf), + } +} +fn run(layer: Layer) { + let mut p = Project::empty(); + p.id = id(1000); + let chain = if layer == Layer::Oa { + Kind::Process + } else { + Kind::FunctionalChain + }; + let work_kind = if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }; + for n in 1..=4 { + element(&mut p, n, chain, layer, None); + } + for n in 10..=13 { + element(&mut p, n, work_kind, layer, None); + } + for (n, owner, work) in [ + (20, 2, 10), + (21, 2, 11), + (22, 2, 12), + (23, 3, 11), + (24, 1, 13), + ] { + occurrence( + &mut p, + n, + layer, + id(owner), + OccurrenceBody::Action { + action: Some(id(work)), + lifeline: None, + }, + ); + } + for (n, owner, target) in [(30, 1, 2), (31, 1, 2), (32, 1, 4), (33, 4, 2), (34, 1, 3)] { + occurrence( + &mut p, + n, + layer, + id(owner), + OccurrenceBody::SequenceReference { + sequence: Some(id(target)), + }, + ); + } + for (n, source, target) in [(40, 20, 21), (41, 21, 22)] { + p.relations.insert( + id(n), + Relation { + id: id(n), + alias: format!("link_{n}"), + name: format!("link_{n}"), + description: String::new(), + body: RelationBody::SequenceLink { + source: id(source), + target: id(target), + guard: String::new(), + }, + }, + ); + } + let exchange = p.relate( + if layer == Layer::Oa { + RelationKind::Interaction + } else { + RelationKind::FunctionalExchange + }, + "X supplies B", + id(13), + id(11), + ); + assert!( + execute_batch(&p, &[]).is_ok(), + "Probe fixture must be admitted by the existing core" + ); + let before = p.clone(); + assert!(!validation::can_connect( + &p, + RelationKind::SequenceLink, + id(24), + id(21) + )); + assert!( + execute_batch( + &p, + &[Command::Connect( + RelationKind::SequenceLink, + "unqualified".into(), + id(24), + id(21) + )] + ) + .is_err() + ); + let (x, r1b, r2b, nested_b, qb) = ( + endpoint(&[], 24), + endpoint(&[30], 21), + endpoint(&[31], 21), + endpoint(&[32, 33], 21), + endpoint(&[34], 23), + ); + let mut index = Index::new(&p, 32, 128); + let first = index.resolve(id(1), &r1b).unwrap(); + let second = index.resolve(id(1), &r2b).unwrap(); + let nested = index.resolve(id(1), &nested_b).unwrap(); + assert_ne!(first.address, second.address); + assert_ne!(first.address, nested.address); + assert_eq!(first.work, second.work); + assert_eq!(first.work, nested.work); + for purpose in [ + Purpose::Precedence { + guard: "pressure > 400".into(), + }, + Purpose::Exchange { exchange }, + ] { + index.connection(id(1), &x, &r1b, &purpose).unwrap(); + } + index + .connection(id(1), &r1b, &qb, &Purpose::CommonWork { work: id(11) }) + .unwrap(); + assert!( + index + .connection(id(1), &x, &qb, &Purpose::CommonWork { work: id(11) }) + .is_err() + ); + assert!( + index + .connection(id(1), &r1b, &x, &Purpose::Exchange { exchange }) + .is_err() + ); + assert!( + index + .connection( + id(1), + &r1b, + &r1b, + &Purpose::Precedence { + guard: String::new() + } + ) + .is_err() + ); + for bad in [ + endpoint(&[], 21), + endpoint(&[30], 23), + endpoint(&[33], 21), + endpoint(&[30, 33], 21), + endpoint(&[24], 21), + endpoint(&[999], 21), + ] { + assert!( + index.resolve(id(1), &bad).is_err(), + "accepted invalid path {bad:?}" + ); + } + let mut depth_limited = Index::new(&p, 1, 128); + assert_eq!( + depth_limited.resolve(id(1), &nested_b), + Err("reference depth limit") + ); + let mut count_limited = Index::new(&p, 32, 1); + count_limited.resolve(id(1), &r1b).unwrap(); + count_limited.resolve(id(1), &r1b).unwrap(); + count_limited.resolve(id(1), &r2b).unwrap(); + count_limited.resolve(id(1), &r1b).unwrap(); + assert_eq!(count_limited.cache.len(), 1); + assert_eq!( + count_limited.resolve_many(id(1), &[r1b.clone(), r2b.clone()], 1), + Err("analysis address limit") + ); + assert_eq!( + count_limited.resolve_many(id(1), &[r2b.clone(), r1b.clone()], 1), + Err("analysis address limit") + ); + assert_eq!( + count_limited + .resolve_many(id(1), &[r1b.clone(), r2b.clone()], 2) + .unwrap() + .len(), + 2 + ); + let mut changed = p.clone(); + if let Some(SharedBody::Occurrence { + body: OccurrenceBody::SequenceReference { sequence }, + .. + }) = changed.elements.get_mut(&id(30)).unwrap().shared_mut() + { + *sequence = Some(id(3)); + } + assert!(Index::new(&changed, 32, 128).resolve(id(1), &r1b).is_err()); + let mut deleted = p.clone(); + deleted.elements.remove(&id(30)); + assert!(Index::new(&deleted, 32, 128).resolve(id(1), &r1b).is_err()); + assert_eq!( + p, before, + "Address resolution may not mutate shared procedures" + ); + println!( + "{layer:?}: existing foreign link rejected; R1/R2/nested distinct; 3 purposes checked; invalid paths/retarget/delete/limits refused; shared model unchanged" + ); +} +fn main() { + run(Layer::Oa); + run(Layer::Sa); + println!( + "PASS: bounded contextual resolver design probe (not production schema/API acceptance)" + ); +} diff --git a/harness/release-artifact-recovery-independent-review-20260906.md b/harness/release-artifact-recovery-independent-review-20260906.md new file mode 100644 index 00000000..ce275f08 --- /dev/null +++ b/harness/release-artifact-recovery-independent-review-20260906.md @@ -0,0 +1,37 @@ +# Artifact recovery independent review — 2026-09-06 + +## Scope and finding + +Read-only review covered artifact lifecycle drafts and UI, actual Owner/Presence slot composition, exact history reset, and semantic recovery admission. The material finding was a fresh artifact picker that asked `Project::layer_of` about an ArtifactLink identity; that API resolves elements/relations, so ordinary artifacts disappeared unless selected/history state reinserted them. The implementation worker changed the test to start from an empty editor and corrected filtering to inspect the artifact owner, including neutral owners. `.local/source6-artifacts-final-tests.log` observed the failure; `.local/source6-artifacts-final-tests2.log` confirms all six artifact tests pass after correction. + +No additional material mutation-scope defect was found. This conclusion is bounded to the inspected code and those tests. Native artifact creation/deletion and minimum-size controls are not established by this review. The Owner candidate picker enumerates all identities without search or pagination; large-model usability remains a limitation to evaluate. + +## Independent stored-payload falsifiers + +Added `crates/oa-desktop/src/semantic_review/artifacts_recovery_falsifier_tests.rs`, registered only as a test module. The fixture authors a reservation and eight explicit reviews through normal addressed events. It then stores version-2 JSON through `State::recovery_snapshot` and restores through `State::from_recovery`. Tests do not construct private recovery DTOs or call their private admission helpers. + +Four tests cover both OA→SA and SA→OA: + +| Test boundary | Explicit probes and oracle | +| --- | --- | +| Reservation admission | Duplicate seed; existing source identity with its correctly corresponding generated alias; wrong owner; wrong alias. Each must refuse restoration. | +| Authored draft admission | Owner encoded with the wrong Raw variant; duplicated exact facet address; unknown artifact payload field. Each must refuse restoration. | +| Destination identity admission | Unreserved nonexistent UUID and an existing element UUID substituted into an artifact draft address. Each must refuse restoration. | +| Legitimate rejected input | Invalid Owner text, absent Revision with retained multiline text, and explicitly present empty Fragment survive exact snapshot round-trip. No command is available until explicit Owner repair. Restored and uninterrupted reviews then prepare identical full projects and canonical sources. Accepted project/source remain unchanged, with no reserved artifact created by restore or preparation. | + +Each refusal also checks the original authored snapshot and accepted project/source remain unchanged and the legitimate invalid draft remains non-executable. Assertions do not depend on a particular diagnostic string. + +## Evidence status + +**Verified: all four tests pass**, covering both transition directions, with 518 other desktop tests filtered out. Windows command (root-authorized exclusive Cargo lease): + +```powershell +. .local/toolchain/env.ps1 +cargo test --workspace --locked artifacts_recovery_falsifier_tests +``` + +`.local/artifact-recovery-falsifier-tests2.log` records exit 0, 22.24 seconds incremental compilation, and 3.36 seconds for the four tests. Other workspace test binaries selected zero tests; this is a focused result, not a new full-workspace pass. The Windows Cargo lease was released immediately afterward. + +The first run compiled successfully but failed all four tests at a fixture helper that incorrectly assumed the checkpoint contained one row. Switching direction correctly retains the prior direction's row. The test helper now finds exactly one row by the checkpoint's active RowAddress before altering its artifact payload. `.local/artifact-recovery-falsifier-tests.log` preserves that initial test-only failure. No production behavior changed; the only existing-source edit is the new `#[cfg(test)]` module declaration. Owned test formatting and scoped diff checks passed. + +These probes do not cover every malformed payload, crash durability, all size limits, or source-version migration. Existing outer fingerprint/size checks were inspected; this addition specifically falsifies the artifact payload admission gap identified during review. diff --git a/harness/release-artifacts-native-20260906.md b/harness/release-artifacts-native-20260906.md new file mode 100644 index 00000000..4aa81b09 --- /dev/null +++ b/harness/release-artifacts-native-20260906.md @@ -0,0 +1,78 @@ +# Artifact lifecycle native evidence — 2026-09-06 + +## Move, optional metadata, conflict and crash recovery + +Actual Iced Windows GUI SHA256 +`AB6ECD62DB02FF3D81E2421EE1DDC926AEF72C65784DD288EAFA281836EC0597` +ran from `.local/native-qa/bin-source6-artifacts`, with isolated appdata and +system-only PATH. This executable predates the later conflict-message wording +correction and Linux journal helper. + +Opened `semantic-artifacts-move.arcadia`, reviewed Interlock requirement rationale, +and explicitly paired the OA and SA Interlock requirement links. Both pickers +displayed the actual link names, stable aliases and current Startup owners. +Set final Owner to SA Safety coordination; independently set Revision to Present +with empty text and Fragment to absent while retaining `section-4` in its draft. +Preview distinguished Empty text from Not set. + +Keep System on Presence conflicted with the Owner move and disabled Apply. Leaving +Presence unselected restored the valid preview. The initial error explained a +conflict but exposed JSON UUIDs and inaccurately implied different review rows; +this prompted a diagnostic-only correction and three passing focused tests in +`release-semantic-conflict-diagnostics-20260906.md`. That wording is not yet native +verified. No model was applied during the conflict. + +At 1440×900 and minimum 1100×700, facet decisions, named owner picker, raw identity +and optional toggles remained usable through normal detail-pane scrolling; action +buttons remained outside the right scrollbar. Replaced Owner identity with `-`: +the draft remained visible, the message requested a valid explicit owner and Apply +was disabled. + +Checkpoint `69352f76-e6ca-4e09-aaa7-7b4d94c5dccc` was preserved as +`.local/native-qa/artifacts-move-before-crash.json`, then process9060 was stopped. +New process10192 Home Recover produced checkpoint +`0b1898f0-4e64-4bec-b26d-c05697435a2b`, copied to +`artifacts-move-after-recovery.json`. Complete structural comparisons of accepted +source and workflow were both equal, covering all three decisions, exact pairs, +invalid Owner and retained optional text. Native inspection showed `-` still +present and Apply blocked. Choosing SA Safety coordination repaired it. + +Apply was clicked once; Save As to +`.local/native-qa/source6-artifacts-move-result.arcadia` matched the complete +independent after-source oracle exactly: +`20BB30473830E44228958E78FB7480269CDCC0580C6C90C718DA75F34CA5A43F`. +One Undo and Save matched the pristine complete source: +`B0FD031671C9F755756922EC5C40A0CE3F40164D22C3C389AEC9FD0705344DA7`. +One Redo and Save matched the after hash again. All three saves succeeded first +attempt, no recovery JSON remained and recovered-run stderr was empty. + +## Scope still outstanding + +Creation with reserved identity and source-absence deletion have preserved +fixtures and automated exact-oracle evidence, but are not yet native accepted. +This report does not claim every artifact owner/type, large-list ergonomics, +Linux application acceptance or post-correction diagnostic rendering. + +## Creation, exclusion, recovery and exact history + +The same AB6ECD62 GUI reserved destination artifact a6f66451-0763-4e4d-a48a-7ee02aac71d1 from OA link f3635689-622e-4e71-849a-ec332f8931f3 under trace f27b20ea-1a66-4a91-87d6-79b7bda09a13. Presence alone blocked Apply until all seven metadata candidates were explicitly reviewed: Name, Description, Owner, Relation, URI, Revision and Fragment. Description retained CRLF; Revision was absent and Fragment was section-4. Eight decisions enabled Apply; excluding the only row retained the draft and disabled Apply. + +Checkpoint450603eb-0275-416d-879d-4faaca089c8b was copied to .local/native-qa/artifacts-create-before-crash.json before controlled stop of process10192. An initial test relaunch mistakenly pointed LOCALAPPDATA at an empty sibling profile; no model input occurred there. After correcting both APPDATA and LOCALAPPDATA to .local/native-qa/appdata, process2540 Home Recover restored the review. New checkpoint07019cff-80f7-42e4-9081-bfae0f05c136 was copied to artifacts-create-after-recovery.json. Complete accepted and workflow structural comparisons were exact, including included=false, all eight decisions, seven drafts and the original reserved UUID. The UI visibly retained exclusion and disabled Apply. + +Including the recovered row enabled Apply. One Apply followed by Save As .local/native-qa/source6-artifacts-create-result.arcadia matched the complete direct-core source oracle regenerated with the reserved UUID. One Undo/Save matched the complete initial source; one Redo/Save matched the complete after source. All three saves succeeded first attempt; zero recovery JSON files remained and stderr was empty. + +- Initial/Undo SHA256: 1E0D662BA33071B122AD9206D2D559A8E748C9431EB3AC1B759BDE3760CA877C. +- Applied/Redo SHA256: 778C099F7CF3187B294E91F1717AB9836656DEEDB09AB6EF01DAEAC55CCD577E. + +The full source equality covers model, artifact identity, metadata and review history; artifact count changes from one to two. The old binary still labels a reserved identity as Deleted reference in facet headings. Row-aware display corrections and independent review are recorded separately; they are not covered by this binary. Native deletion remains pending. + +## Source attachment absence and destination removal + +Process2540 on the same AB6ECD62 binary opened semantic-artifacts-delete.arcadia. Traceability showed the OA link now under OA Safety coordination and the SA link still under SA Startup coordination. The explicit OA picker offered its existing identity with the current owner and wrapped 'not attached to reviewed owner' label after selection. The SA picker offered the exact existing destination identity. Staging the pair showed OA Presence No / SA Presence Yes. Take Operational changed the validated destination preview to No; one Apply removed only the destination link, leaving the OA card and its Safety owner visible. + +Save As .local/native-qa/source6-artifacts-delete-result.arcadia matched the complete direct-core after source exactly. One Undo restored both cards and unreviewed trace, and Save matched the exact original. One Redo restored the removal/reviewed linked state and Save matched the exact after source. All three saves succeeded first attempt; recovery JSON count zero and stderr zero bytes. + +- Applied/Redo SHA256: E50FC462EC22DBF98011929931DD2489430001C3E6433608685B9D88BCEB9ACB. +- Initial/Undo SHA256: EBA2AEC39E6EFB405EACE4BC8AB0C5563907C26D449E71A4AA750DF9F73233B4. + +This closes the actual moved-source-attachment removal fixture, including all eight acknowledged facts in the exact history oracle. A no-source-identity destination-only row was not separately applied natively in this run; its retention remains covered by the automated artifact absence cases. Across MOVE/CREATE/DELETE, nine native saves passed first attempt. Earlier pending wording above is historical. diff --git a/harness/release-attachments-native-20260906.md b/harness/release-attachments-native-20260906.md new file mode 100644 index 00000000..36ce645e --- /dev/null +++ b/harness/release-attachments-native-20260906.md @@ -0,0 +1,41 @@ +# Recovered Child/Incident review — native Windows, 2026-09-06 + +The real Iced app at `.local/native-qa/bin-source6-recovery/arcadia-studio.exe` +(SHA256 `DD5D01FA07B921F1A7B365C33867876A7AE1BCBD01815B4BB4206408D2012F95`) +ran with isolated appdata and a system-only PATH. This executable includes +source-6 attachment review and checkpoint upgrade, but predates artifact lifecycle +controls and the later diagram-inspector selection correction. + +Using the tracked `fixtures/release1-native/semantic-attachments.arcadia` fixture, +the Startup ownership rationale row paired the two Verify interlock children and +set the SA child's Parent to SA Safety coordination. A second included row, +Navigation endpoint rationale, paired the OA/SA Request response associations and +set only the SA association's Source to SA Validated navigation request. The +preview had two candidates and no impact approvals. The actual named pickers, +pair actions and candidate controls were usable at 1440×900 and 1100×700; the +detail pane scrolls normally. This is scoped evidence, not exhaustive layout QA. + +The process was stopped with this unapplied two-row draft. Before/after snapshots +are `.local/native-qa/attachments-before-crash.json` and +`attachments-after-recovery.json`. Home Recover restored both included rows and +all four explicit reference identities. Structural comparisons found both the +raw workflow and accepted project unchanged. The recovered active trace picker +showed the actual Navigation endpoint rationale name and alias, verifying the +previous stale/deleted-label correction. + +Apply reviewed changes was clicked once. The visible tree placed SA Verify +interlock below SA Safety coordination; checks decreased from 17 to 15. Save As +to `.local/native-qa/source6-attachments-result.arcadia` succeeded first attempt +and matched the complete independent `.after.arcadia` oracle byte for byte: +`9D084DD75A0251D349CC31F9EE98DF58D0BC76859DC3CB1503F242E2F759888A`. + +One Ctrl+Z and Save restored the entire original source exactly: +`8740F27BE99624013D9BE876639C0411E1B8C14F0F9A843F43DC71D769EFD79A`. +One Ctrl+Shift+Z and Save restored the exact after hash above. All three saves +succeeded on the first attempt. No recovery JSON remained (only recent-models +metadata), and the saved app then closed cleanly. + +Invalid raw attachment identities and competing direct Keep decisions are covered +by automated tests; they were not entered during this native run. Artifact +creation/deletion/move, other semantic facet families and Linux acceptance remain +separate gates. diff --git a/harness/release-behavior-transition-context-20260906.md b/harness/release-behavior-transition-context-20260906.md new file mode 100644 index 00000000..294d5128 --- /dev/null +++ b/harness/release-behavior-transition-context-20260906.md @@ -0,0 +1,41 @@ +# Behavior transition workspace and placement review + +## Native finding and cause + +Root observed this on the staged `246B…` desktop: applying an SA scenario Whole subtree → OA transition left Behavior, selected the first reused **Rescue team** participant, and added the scenario, two lifelines and two occurrences to the unrelated **Draw operational communication** architecture view. Count-based placements overlapped existing content and produced three amber route warnings. The legacy native oracle predicts those presentation edits; that does not make them an appropriate user workflow. + +Three desktop seams caused the result: `workflow.rs` appended placements for every transitioned element after the core plan; `app.rs` selected the first dependency-ordered plan change instead of the explicitly requested root; the same success branch unconditionally navigated to Architecture. The shared transition planner already honors an explicit destination diagram and does not require the extra implicit placements. + +## Approved bounded correction + +When the first explicitly requested root belongs to a scenario, process/functional chain, or state machine (including nested elements and typed behavior relations), and no destination diagram was requested, the desktop returns the exact core transition candidate. It neither creates a view nor adds nodes to an existing architecture view. Explicit destination requests retain the existing placement contract. Ordinary architecture/work transitions retain their existing automatic placement behavior. + +The application selects the counterpart of that explicit root, resolves its nearest behavior owner, and displays Behavior. A state machine without a saved state view offers the existing **Create diagram** action. The application clears an obsolete architecture node selection and resets state-camera context only when the resolved owner/view changes. Reusing the same behavior context retains its camera. Typed StateAction relations now resolve through their source state, just as StateTransition and SequenceLink resolve through their source element. + +The operation remains one `Command::Replace` and the existing document source transaction. Core planning, scope, dependency closure, tracing and history semantics are unchanged. Failed source writeback retains the review and navigation. Existing modal and pending-draft guards remain in force. + +## Automated evidence + +Five focused App tests in `app/transition_context_tests.rs` cover: both directions with a reused dependency preceding the explicit scenario root; nested lifelines and process/chain roots without any view; explicit destination placement versus ordinary work placement; machine/typed-action ownership and repeated same-context camera retention; rejected source writeback and an invalid property draft blocking review start. The scenario test compares the complete accepted project against the independently applied core plan and asserts exact source/model Undo and Redo in one history operation. + +With the Linux compile explicitly paused, all five focused tests passed on their first compilation (0.63 seconds runtime, 530 desktop tests filtered). Strict workspace/all-target Clippy passed in 26.79 seconds; the workspace binaries built in 50.85 seconds. Rustfmt and scoped `git diff --check` also pass. Logs are `.local/behavior-transition-context-tests.log`, `behavior-transition-context-clippy.log` and `behavior-transition-context-build.log`. This was a focused test run, not a fresh full-desktop run. The semantic-review worker's independent source review found no material defect; see `release-behavior-transition-independent-review-20260906.md`. Stale pre-transition scenario selection events are tested after Apply and after Undo. + +The tested stage is `.local/native-qa/bin-source6-behavior-context`. Desktop SHA-256: `F5ECAEC02BBAFBC6F3BDC6ADEB746AFE91F3A495C6D9564A68DEA41E61AC9246`; server: `1F98F02DBC9FB2D9AFA3C42BB0CB4038BAAE81C85EF17365B5CB486669E749F8`; libunwind: `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4`. It contains the corrected whole-folder copy guidance and all 37 teaching files, including 30 source files; each copied lesson file was hash-compared with the working source. Manifests: `.local/behavior-transition-context-staged-sha256.txt` and `behavior-transition-context-staged-files.json`. The earlier `246B…` native binary and immutable Linux snapshot precede this correction. The Windows Cargo lease was released immediately after build completion, before staging. + +## Corrected reverse native gate — PASS + +Root operated process 9672 on the `F5ECAEC…` binary at the default 1440×900 size, starting from the same preserved `cm-reverse-scenario.arcadia` baseline. Whole subtree confirmed both participant mappings and the boundary mapping, with both OA participant Names retained. Apply immediately stayed in Behavior and selected scenario `@oa_cm_200`. The mapped participant lifelines, solid leftward Call and dashed rightward Reply were visible. Manually switching to Architecture showed the original four readable objects, without overlap or route warnings. + +Save As to `.local/native-qa/cm-reverse-behavior-after/entry.arcadia` passed the separate independent `check reverse-behavior` oracle: complete Project and exact reachable source equality with ten generated UUID substitutions and zero placement identities. The accepted three-file bundle is preserved in `cm-reverse-behavior-accepted`. The legacy fifteen-UUID/five-placement oracle is retained separately, not weakened or overwritten. + +One Undo and Save restored the baseline entry exactly (`1ABC4C4CAC37CFB06ED3B12A115F9E7E6A2D3AFFD34B25C355309E5EE465FA40`). One Redo and Save restored all **three reachable files** exactly: + +| File | SHA-256 | +| --- | --- | +| `entry.arcadia` | `EC826B9B84616507490AC30E4D57100DD6338DBA2F7071E879A8FB58820A85C5` | +| `traceability.arcadia` | `A56BFEFBCA3CE2259E5EB3B374A4BF6BDD9E2F23442ADEA5892791993997F0FC` | +| `oa/behavior/scenario_oa_cm_200.arcadia` | `87DF2C4A62118C326FC0B3A5768075754B643B05EDBC596C0DB5F81A903EC42A` | + +The reviewer independently re-read these final file hashes for this report. Root reopened through Home recents and observed both the original architecture and mapped scenario cleanly. All three Save submissions succeeded on their first attempt; the first Save As filename focus/Ctrl+A/type sequence worked on the first attempt. Stderr and recovery checkpoint counts were zero. + +This closes the reported reverse-scenario navigation, architecture-preservation and Save/Undo/Redo/reopen gate on this binary. Ordinary architecture transitions and explicit destination requests have focused automated regression evidence; they were **not separately native-tested in this corrected trial**. diff --git a/harness/release-behavior-transition-independent-review-20260906.md b/harness/release-behavior-transition-independent-review-20260906.md new file mode 100644 index 00000000..dcbfd663 --- /dev/null +++ b/harness/release-behavior-transition-independent-review-20260906.md @@ -0,0 +1,59 @@ +# Behavior transition navigation — independent review, 2026-09-06 + +Reviewed the bounded changes in desktop `workflow.rs`, the workflow Apply arm in +`app.rs`, `behavior.rs::selected_behavior`, and the five new +`app/transition_context_tests.rs` cases. No production edits, Cargo, or native +input were performed by this reviewer. + +## Findings + +No material correctness defect found in the inspected change. The adapter now +finds the change whose source is the explicit request root, rather than selecting +the first dependency. A behavior-owned root with no explicit destination returns +the core candidate before legacy architecture placement. Explicit destinations +still follow the core/placement path, while ordinary work keeps its architecture +behavior. StateAction resolves the behavior owner through its typed source state. + +The app resolves the counterpart's owner from the candidate, skips port-owner +reveal for behavior navigation, and changes page/selection only after the single +`Document::execute(Replace)` succeeds. A rejected source writeback restores the +review without changing navigation or accepted history. The previous/current +owner and saved state-view identity comparison preserves camera state when that +context is unchanged. The normal scenario/state event contexts also include +document, revision and owner/view identity, so the new route does not make old +canvas events authoritative. + +The new tests use a deliberately dependency-first scenario in both directions, +compare the complete app candidate with core, preserve every unrelated diagram, +and verify exact accepted source and one Undo/Redo boundary. Other tests cover +internal selection, process/chain roots without existing views, explicit +destination versus ordinary work, machine/typed-action owner and camera behavior, +and rejected source preflight plus retained invalid property input. The author +also added the suggested stale-event falsifier: the both-direction test captures +a source-scenario selection event before Apply, delivers it after Apply and Undo, +and asserts unchanged model/selection. That addition was reviewed read-only; +this review does not claim the new tests have executed. + +## Independent fixture oracle + +Added distinctly named `check reverse-behavior` and +`generate-reverse-behavior` modes to the tracked CM fixture generator. The new +`cm-reverse-behavior.after-template` preserves the original before-source bundle +and every saved diagram. Legacy `check reverse` and its template remain intact. +See [fixture instructions](fixtures/release1-native/communication-means/README.md). + +A coordinated tiny direct-rustc build linked the stable existing core/serde +libraries into an ignored executable, without touching Cargo outputs. Results: + +- New behavior template: complete model/source pass, ten generated target/trace + UUID substitutions, zero placement substitutions. +- Legacy template: complete model/source pass, fifteen substitutions including + five placement identities. +- Each policy rejects the other's placement outcome; added source whitespace is + separately rejected. Regenerating the new bundle is byte-exact. + +The checker never adopts observed geometry, routing, metadata or names. Negative +logs: `.local/native-fixtures/cm-reverse-behavior-falsifiers/`. Root's completed +legacy native result remains valid evidence for that older policy. Corrected +native Behavior routing, visible Call/Reply selection and its Save/Undo/Redo +sequence are a separate pending gate, as are the new desktop test run and build. diff --git a/harness/release-chain-assembly-contract-independent-review-20260906.md b/harness/release-chain-assembly-contract-independent-review-20260906.md new file mode 100644 index 00000000..b5c8c2a7 --- /dev/null +++ b/harness/release-chain-assembly-contract-independent-review-20260906.md @@ -0,0 +1,126 @@ +# S1 contract independent review — 2026-09-06 + +Read-only review of `release-chain-assembly-contract-probe-20260906.md`, its +isolated resolver probe, current validation/reference/source/recovery seams, and +the proposed S2 contract. No production edits, Cargo, or native input. + +The central representation is sound: explicit caller owner, ordered reference +**occurrence** IDs, and a leaf occurrence distinguish repeated and nested uses. +Keeping old direct SequenceLink unchanged avoids inventing historical semantics. +The three closed purposes appropriately avoid equating common work with merged +execution. The following corrections should be frozen before production wiring. + +## 1. Deterministic admission must not depend on resolver cache history + +**P1 contract correction.** `Index::resolve` in the probe rejects a new address when +`cache.len() >= max_addresses`. The contract proposes sharing the accepted-revision +index with GUI endpoints, validation and other consumers. Together, those choices +can make a model's apparent validity depend on which preview addresses were visited. + +Minimal falsifier: max_addresses=1; authored model needs only valid address B. +Fresh index resolves B. On the same accepted Project, first preview unrelated +valid A, then resolve B: the probe returns `resolved address limit`. Reversing the +interaction order changes the result without changing the model or authored budget. +The probe demonstrates a cache cap, not yet deterministic model admission. + +Separate three budgets: deterministic counts of **authored** path components, +per-request/per-analysis expansion work, and cache storage capacity. Evict or skip +cache insertion when cache capacity is reached; never turn a cache miss into a +structural-invalid endpoint. Oversized requested expansion may explicitly refuse +or return an incomplete-analysis notice. Test cold/warm/evicted cache and changed +preview order yielding identical structural results and exact endpoints. + +## 2. Shared source7 must freeze nested historical message serialization + +**P1 compatibility correction.** S1 alone adds a new constructor and can preserve +existing element serialization. S2 adds `operation` inside existing Exchange +occurrence bodies. The combined migration cannot reconstruct old fingerprints by +changing only Project.schema_version and borrowing all current `p.elements`. + +Current `recovery/source5.rs::Project5` deliberately serializes borrowed elements; +after a normally serialized default `operation: None` is added, an old message gains +`"operation":null` in those bytes. That changes its fingerprint even though the +accepted source has only a version-token change. The current empty-element literal +fingerprint test would not catch this. Current semantic recovery also explicitly +clears impact approvals on the admitted upgrade; preserve that behavior, rather +than treating unchanged engineering facts as proof that old plan approval survives. + +Use one coordinated version7 admission/migration owner. Frozen source5 and source6 +views must omit new message fields and reject new constructors, including explicitly +empty/default new fields under old advertised versions. Either a historical +serializer or carefully proved omission policy can preserve the old byte shape; +neither should be assumed from serde defaults alone. Pin old5/6 literal Projects +containing Call, Reply, values, direct SequenceLink, semantic history, and a saved +state/view selector, plus invalid raw workflow/property/source drafts. Verify exact +old hashes before restoring/replanning; do not rewrite raw invalid buffers. +New source7 combined S1+S2 snapshots need separate exact restore tests. This is a +coupled migration gate, not two independent schema-number increments. + +## 3. Freeze deletion versus retarget refusal; never shorten an address + +**P2 contract ambiguity.** The probe correctly rejects paths after referenced +records are removed/retargeted, but that does not define the normal Delete command's +accepted result. The proposed DTO has no unresolved owner/path/leaf representation. +The implementation must choose explicit cleanup or reject the whole command; it +cannot simply drop a deleted path component and reinterpret the same leaf directly. + +Smallest consistent policy: normal Delete of caller/path reference/leaf removes +each dependent ContextualLink in the same reference-cleanup transaction, retaining +unrelated links and reusable definitions. Reidentify rewrites every exact typed +component. A generic reference-target edit that would invalidate a surviving link +rejects; an explicit previewed repair batch may delete/replace that link. Deleting +an assembly-purpose exchange/work similarly removes or explicitly repairs its +dependent link, never changes its purpose to Precedence. Freeze this policy or a +clearly specified alternative, then test Undo/source restoration and R2 isolation. + +Also test caller-owner deletion when both leaves survive in external procedures: +using generic leaf source/target accessors alone will miss that ownership edge. +The accepted owner/reference visitor must include owner, each path member, leaf, +and purpose work/exchange. A source module update must follow caller ownership, +not the defining module of a surviving leaf. + +## 4. Define which edges feed analysis before declaring the graph complete + +**P2 semantic decision.** The contract distinguishes Precedence from Exchange and +CommonWork, but does not yet state which feed entry/exit/reachability/control +cardinality and behavior-coverage diagnostics. Current `validate_sequence` builds +direct SequenceLink adjacency; treating every new purpose as another execution +edge could create false split/merge warnings or suppress missing-coverage warnings. +Ignoring all contextual edges could report the accepted R1.B connection unreachable. + +Freeze separate projections: contextual precedence adjacency, exchange/work +assembly incidence, and coverage evidence. Specify how direct links inside a +referenced procedure are qualified into each caller instance and how an outer +reference-node predecessor relates to entry/exit boundaries; do not count the +collapsed preview and its expanded contributions as two executions. Keep shared +P's own diagnostics separate from caller O's context. On analysis limits, emit an +explicit incomplete result, not success or guessed missing-path warnings. + +A bounded discriminating test can use exactly R1/R2, A→B→C and X→R1.B, then add +CommonWork and Exchange purposes independently. Verify their intended diagnostic +effects and unchanged standalone P/R2 facts. This does not require a simulator or +a general execution semantics framework. + +## Additional implementation checks, not new scope + +- Generic source/target accessors returning leaf IDs are safe only after auditing + caller-owner consumers. New contextual self-links must compare full addresses: + R1.B→R2.B is not a self-link merely because the leaf ID is equal. Behavior + navigation must select the explicit caller, not the leaf's defining procedure. +- Transition closure needs caller, every reference occurrence/target sequence, + leaf, purpose work/exchange and their mappings. A target path is mapped component + by component and re-resolved; neither the global work ID nor the first mapping + is enough. Include two references to one callee and a conflicting retarget case + in one frozen-base semantic-review test. +- Qualified source/target/purpose are coupled values. Semantic review can expose + closed facets but must validate the complete composed relation and compare actual + slots across rows; owner/path changes cannot silently reinterpret old leaf drafts. +- The probe's Precedence branch currently returns Ok without production guard-text + or work/exchange adjacency checks, and it does not enforce the proposed ban on + all-direct contextual Precedence. Its pass is endpoint-resolution feasibility, + not complete admission parity. Add those decisive negatives when implementing; + retain existing direct SequenceLink tests unchanged. + +No further material address-identity defect was found in this bounded read. The +proposal remains viable after these decisions; none requires broadening S1 into +scenario gates, physical paths, scheduling, or arbitrary cross-owner edges. diff --git a/harness/release-chain-assembly-contract-probe-20260906.md b/harness/release-chain-assembly-contract-probe-20260906.md new file mode 100644 index 00000000..56d9dfe3 --- /dev/null +++ b/harness/release-chain-assembly-contract-probe-20260906.md @@ -0,0 +1,149 @@ +# S1 contextual chain links — contract and isolated probe + +Status: proposed contract for root synthesis, not implemented production language or API. The accepted expressivity gap is documented in `release-chain-assembly-independent-review-20260906.md`. This phase changes harness artifacts only. + +## Recommended representation + +Retain the exact existing `RelationBody::SequenceLink { source: Id, target: Id, guard: String }` representation and direct-owner meaning. Add **one** `RelationKind::ContextualLink` / `RelationBody::ContextualLink` constructor for caller-owned links with qualified endpoints. Proposed Rust DTOs: + +```rust +// All structs deny unknown fields. No default missing owner/path/leaf/purpose. +struct OccurrenceEndpoint { + references: Vec, // ordered SequenceReference occurrence IDs, outermost first + occurrence: Id, // direct leaf contribution in the final resolved sequence +} +enum ContextualLinkPurpose { + Precedence { guard: String }, + Exchange { exchange: Id }, + CommonWork { work: Id }, +} +// Variant of the existing RelationBody, retaining normal relation metadata/ID. +ContextualLink { + owner: Id, + source: OccurrenceEndpoint, + target: OccurrenceEndpoint, + purpose: ContextualLinkPurpose, +} +``` + +The source/target accessors required by generic relation consumers can return the leaf occurrence IDs, but **must not be used to infer this relation's caller owner**. Add a shared `contextual_owner` / resolved-endpoint accessor and audit every owner/context consumer. An explicit owner avoids guessing whether a leaf's defining chain or the first reference's caller owns the connection. At least one endpoint must have a nonempty reference path for a new Precedence contextual record; ordinary direct precedence remains SequenceLink, so there is one canonical stored spelling for it. Exchange and CommonWork purposes can connect direct or nested Action contributions. + +Every path component belongs directly to the current resolved sequence, is a resolved SequenceReference, and targets another same-layer sequence of the same kind. A leaf is a direct contribution in the final sequence. Repeated references to one sequence are distinct instances; equal work IDs or equal leaf IDs do not imply equal addresses. Equality/self checks use `(owner, references, occurrence)`. Reuse cycles reject; ordinary permitted topology cycles remain distinct. + +For Precedence, the guard stays opaque authored text and existing appropriate contribution-kind/adjacency rules apply after resolving endpoints. This purpose asserts contextual precedence and creates no engineering exchange. Exchange requires an existing same-layer Interaction (OA) or FunctionalExchange (SA), and both endpoint leaves must be Actions referencing its source and target work. CommonWork requires both endpoint leaves to be Actions referencing the exact same existing work ID. Neither assembly purpose has a guard field: a conditional precedence constraint is a separate typed link, not an invented property of every involvement link. CommonWork does not assert a merged execution or remove either occurrence. + +### Why exactly these three purposes + +The official [Creation of Functional Chains and Physical Paths](https://raw.githubusercontent.com/eclipse-capella/capella/master/doc/plugins/org.polarsys.capella.ui.doc/html/05.%20Diagram%20Management/5.5.%20Creation%20of%20Functional%20Chains%20and%20Physical%20Paths.mediawiki) provides the concrete justification: + +| Purpose | Primary section and requirement | +| --- | --- | +| Precedence | Lines 52–64 describe SequenceLinks as contextual ordering with an optional condition. Lines 99–105 explicitly describe folding links whose endpoints are hidden inside referenced chains, including SequenceLinks. | +| Exchange | Lines 79–94 describe assembly through an exchange, using involved functions that need not be chain boundaries. | +| CommonWork | Lines 88–96 describe assembly between two function involvements referencing the same function. | + +These are three distinct authored meanings, not three hypothetical future features. The official text does not establish that a common-function link fuses executions, so this contract deliberately leaves that unsupported inference out. Control-node variants, physical paths, scenario gates and runtime scheduling are not added by this proposal. + +## Concrete JSON, DSL and HTTP shapes + +Proposed schema **7** record (owner ends `0001`, direct X occurrence `0018`, R1 reference `001e`, inner B occurrence `0015`, engineering exchange `0050`): + +```json +{ + "id": "00000000-0000-4000-8000-000000000090", "alias": "supply_r1_b", "name": "Supply B in first repair", + "description": "Caller-local assembly", + "body": { + "ContextualLink": { + "owner": "00000000-0000-4000-8000-000000000001", + "source": {"references": [], "occurrence": "00000000-0000-4000-8000-000000000018"}, + "target": {"references": ["00000000-0000-4000-8000-00000000001e"], "occurrence": "00000000-0000-4000-8000-000000000015"}, + "purpose": {"Exchange": {"exchange": "00000000-0000-4000-8000-000000000050"}} + } + } +} +``` + +For nested reuse, the target references vector is `[R1_REFERENCE_UUID, INNER_REFERENCE_UUID]`. For common work, use `{"CommonWork":{"work":"B_WORK_UUID"}}`. For qualified precedence, use `{"Precedence":{"guard":"authorization received"}}`. These closed variants make invalid mixtures impossible to accept as ignored fields. + +Proposed source declaration, following the existing structured-value syntax: + +```text +arcadia 7; +project repair "Repair coordination" { + // Existing declarations are omitted from this illustration. + contextual_link supply_r1_b "Supply B in first repair" id "00000000-0000-4000-8000-000000000090" { + description "Caller-local assembly"; + owner @outer; + source { references []; occurrence @x_occurrence; }; + target { references [@r1]; occurrence @b_occurrence; }; + purpose { Exchange { exchange @x_to_b; }; }; + } +} +``` + +This is proposed syntax, not a file accepted by the current source-6 parser. Generic structured values should carry it without a second ad hoc path grammar. Reference aliases are resolved by the normal source identity mechanism, including every array member. The caller owns the declaration's source location; editing the contextual link must not move or rewrite the referenced procedure module. + +Use the existing `POST /api/v1/projects/{id}/transactions` with `If-Match`, body `{"commands":[{"CreateRelation": RECORD}],"dry_run":true}`; after inspection, submit the same ordinary transaction with `dry_run:false` and the required current ETag. `EditRelation`, normal deletion/reidentification, source import/replacement and transition planning use the same core contract. No special assembly endpoint or client-only mutation is required. Actual JSON/OpenAPI fixtures must use literal valid UUIDs and demonstrate valid direct-to-nested and nested-to-nested addresses, wrong path/kind/layer, reversed exchange endpoints, common-work mismatch and stale revision. Preview/refusal must leave source and revision unchanged. + +## Alternative: extend SequenceLink with default-empty paths + +An alternative retains source/target leaf IDs and adds `owner` plus `source_path`/`target_path` and a purpose discriminator to SequenceLink. Empty paths and default Precedence can retain old meaning; serde `skip_serializing_if` would be needed to retain historical JSON bytes. This is viable only with strict advertised-version admission and a compatibility serializer for old fingerprints. + +The alternative loses on the current integration surface: every old SequenceLink destructuring/constructor changes; permissive serde defaults can accidentally admit new properties under old source versions; default purpose/owner reconstruction risks converting omitted fields into guessed semantics; and old model/semantic-review snapshot serialization becomes harder to prove unchanged. Adding paths alone still fails common-work versus exchange assembly, so it is not actually the complete smaller contract. The new constructor keeps all old record shapes exactly unchanged and limits new owner semantics to one clearly identifiable case. It still requires auditing generic source/target consumers; the probe does not pretend otherwise. + +## Version and checkpoint plan + +Use source/interchange **7**, even though source 6 is an uncommitted release addition: real source-6 files and private recovery snapshots already exist. Preserve their exact interpretation. Freeze the source-6 constructor set and admit ContextualLink only in 7. Keep all source 1–5 closed admission and migration behavior; neither an explicit empty new path nor the new constructor is admissible there. Never reuse SequenceLink's old guard or endpoint fields to encode a contextual path. + +The 6→7 migration changes only the advertised version in the project and the entry's version token. It inserts no model record, ID, link, default path, baseline or review decision; no include/trivia/file bytes change otherwise. Source5→current migration must continue preserving its existing facts and default view-options behavior. Update historical migration tests by checking their actual preserved facts, not simply accepting a new aggregate hash. + +Private recovery is a release gate, not post-release cleanup. Current `recovery/source5.rs` explicitly accepts only a verified `(5,6)` token-only upgrade and reconstructs historical project/diagram serde order; it cannot silently accept `(6,7)` or `(5,7)`. Generalize this narrowly with version-specific legacy fingerprint adapters. For 6, preserve the old Project field order and all existing record bytes, using schema_version 6 and refusing any ContextualLink in the historical model shape. For 5, retain the existing omitted `Diagram.view_options` representation and exact old hash. Verify the complete accepted source bundle differs only at its version token before authorizing a fingerprint rebind. Raw invalid source/property/behavior/workflow buffers must remain exact; only clean baseline buffers may adopt the token change. + +Restore no saved executable command or previously accepted plan. Restore the raw typed review decisions under their exact old identity/fingerprint, then replan against the migrated accepted model through the existing recovery mechanism. Preserve explicit approvals only where the current compatibility contract already safely retains them after verified fact-preserving migration; never synthesize new approvals for ContextualLink. Test old source5 and source6 checkpoints with multiline invalid raw drafts, excluded rows and prior explicit decisions, stale fingerprints, forged accepted bytes, unknown new constructors, detached Save As and no original-file overwrite. Changing the global schema constant without these tests would regress an already accepted workflow. + +## One index, explicit limits, no eager Cartesian expansion + +Add one core `topology_context` module. At each accepted revision build direct owner/contribution/reference/work/exchange indexes once. Resolve only authored or requested addresses with iterative path validation and memoize by full address. Existing direct SequenceLinks adapt to empty-path resolved edges; new links resolve through the same entry point. GUI endpoints, validation, transition closure, source reference/lifecycle handling and semantic review must use this resolver rather than duplicate path walking or owner inference. + +Proposed default service/desktop policy for the first bounded implementation: maximum path depth 64; count all path components against the existing 50,000 ordered-reference admission budget, together with ordinary step references; at most 50,000 distinct resolved contextual nodes in an expanded per-owner analysis and 100,000 resolved edges. These are caller resource limits, not new execution semantics, and must be published/tested before freezing. The probe uses smaller configurable limits to falsify enforcement. Structural validation of authored endpoints is linear in their total supplied path length and does not require expanding all combinations of repeated reuse. + +Independent review found an error in the first probe's resource model: using current cache length as an address-admission limit makes a valid address fail after unrelated preview activity. The corrected design separates deterministic authored/per-analysis budgets from cache residency. Cache capacity causes eviction or uncached computation, never an invalid-model result. Analysis limits count the current requested unique addresses independently of prior cache contents or traversal order. The revised probe checks both warm-cache orders with capacity one and a separately bounded two-address request; the corrected direct-rustc rerun passed for OA and SA. + +Deletion policy proposed for root ratification: normal Delete of an owner, leaf or any reference-path component cascades the **whole dependent ContextualLink**, preserving its source/history restoration through ordinary Undo. It never trims an address. Retargeting a reference is an edit, so a now-invalid contextual link rejects unless the same explicit batch repairs or deletes that link. Source replacement must supply a coherent final graph; it does not receive an inferred path repair. + +Graph projections also need an explicit boundary: precedence/control diagnostics consume SequenceLink plus ContextualLink::Precedence, without turning CommonWork into an execution merge. Exchange/CommonWork assembly incidence is separately available for assembly connectivity and graphical labels. Existing behavior coverage follows actual involvement/reuse identities; it must not turn a caller-only link into an edit of the shared procedure. Caller-qualified warnings carry owner/path/leaf provenance. Root must ratify how assembly-incidence affects any new completeness diagnostics; the resolver probe does not establish those graph-analysis semantics. + +Graphical expansion can be paged/lazy and report an explicit visible/total count. It must never claim a truncated graph is complete. If completeness analysis exceeds its bounded expansion, emit a located analysis-limit notice and retain exact structural validity rather than inventing unreachable/entry/exit warnings from a partial graph. The API may reject an explicitly requested oversized expansion; it must not silently return a complete-looking answer. Root should ratify these limits after a repeated-reference scale probe before implementation acceptance. + +## Isolated probe result and reproducibility + +Tracked source: `harness/probes/chain_context_resolution.rs`. It defines experimental address/purpose/index structs outside oa-core and builds valid OA and SA fixtures using the existing production types. Existing core execution first accepts each complete fixture and rejects the unqualified cross-owner SequenceLink. The experimental resolver then checks r1 versus r2 and two-level nesting, all three purpose constraints, wrong/missing/skipped paths, wrong exchange direction/common work, contextual self-link, retarget/delete refusal, configurable depth/address budgets, cache hits and exact unchanged shared Project. + +Direct rustc with warnings denied **PASS**, both OA and SA. Combined compile/run took about 5.3 seconds. No Cargo/shared target mutation occurred; available host memory before compile was 1,470 MiB. Logs: `.local/chain-context-resolution-build.log`, `.local/chain-context-resolution-run.log`; executable `.local/chain-context-resolution.exe`. The stable linked core artifact was `liboa_core-6c8da57367eec642.rlib` (last written 19:48:55 local). + +That initial pass established address feasibility but retained the subsequently identified cache/admission defect. It is not acceptance of the corrected cache policy. The probe's Precedence branch also does not implement production guard/adjacency validation or the proposed all-direct spelling restriction; those remain explicit core implementation tests, not inferred passes. + +Corrective rerun: direct rustc with warnings denied and both OA/SA executions **PASS**, about 7.2 seconds combined, 1,951 MiB available before compilation. Logs `.local/chain-context-resolution-build2.log` and `.local/chain-context-resolution-run2.log` supersede the first run for the cache/analysis-budget assertions. This rerun creates only the ignored executable/logs; no Cargo or shared production/target mutation occurred. The remaining feasibility limits above still apply. + +```powershell +. .local/toolchain/env.ps1 +rustc --edition 2024 -D warnings harness/probes/chain_context_resolution.rs ` + --crate-name chain_context_resolution ` + --extern oa_core=.local/target-gnullvm/debug/deps/liboa_core-6c8da57367eec642.rlib ` + -L dependency=.local/target-gnullvm/debug/deps ` + -o .local/chain-context-resolution.exe +& .local/chain-context-resolution.exe +``` + +Another workspace may have a different hashed rlib filename; use its existing matching core/dependency build. This probe establishes endpoint-resolution feasibility, not production serialization, rendering, full graph analysis, migration/recovery, transition review or HTTP acceptance. Those remain required gates. + +## Proposed implementation ownership + +| Owner | Exclusive files/seams after root grants the implementation phase | +| --- | --- | +| Core contract worker | New `oa-core/src/topology_context.rs` and tests; existing model/accessors, validation, references/reidentify/delete, schema7/persistence/source migration, transition closure/mapping and core semantic exact-slot facets. One worker owns existing shared core files. | +| Behavior/UI worker | `behavior/topology.rs`, `topology/{direct,view}.rs`, `app/process_canvas.rs`; qualified hit targets and contextual pickers/inspectors, folding anchors and pending/stale-event tests. Shared `app.rs`/forms seams only after explicit coordination. | +| Semantic/recovery worker | Existing semantic review adapters and old5/6 checkpoint compatibility plus focused forged/raw-recovery tests; consume core resolver/facets, do not create a parallel endpoint interpretation. | +| Root/integration | Source/API specification, generated OpenAPI/example checks and HTTP fixture tests, package/teaching scope, build leases, native OA/SA repeated+nested reuse/Save/Undo/Redo/reopen gates. | + +First implementation checkpoint should be core DTO/resolver/version admission plus executable counterexample/migration tests, before GUI behavior is built on it. Do not commit to the new representation merely because this design probe compiles; root must synthesize the version/purpose/limits decisions and the remaining reviewers may falsify them. diff --git a/harness/release-chain-assembly-independent-review-20260906.md b/harness/release-chain-assembly-independent-review-20260906.md new file mode 100644 index 00000000..e286e390 --- /dev/null +++ b/harness/release-chain-assembly-independent-review-20260906.md @@ -0,0 +1,77 @@ +# Independent chain-assembly expressivity review + +## Conclusion and scope + +The gap is real: Studio cannot currently author a connection to one particular interior contribution of one particular reused process/chain. This loses a useful OA/SA modeling relationship, rather than merely using different Capella class names. It should block a claim of the requested fully feature-complete OA/SA release until implemented or explicitly accepted as a narrower product scope by the user. It does not invalidate the already demonstrated procedural-reference workflows, and it does not justify implementing Logical/Physical Architecture or the entire Capella metamodel. + +This is a read-only conceptual/code review, independent of the initial scope reviewer. No production edits, Cargo, native input or new runtime probe occurred. Existing source and regression tests establish current admission limits; the counterexamples below are reasoning about representable facts, not claims of a new executable behavior engine. + +## Primary evidence + +The official [Creation of Functional Chains and Physical Paths — Assembling Functional Chains](https://raw.githubusercontent.com/eclipse-capella/capella/master/doc/plugins/org.polarsys.capella.ui.doc/html/05.%20Diagram%20Management/5.5.%20Creation%20of%20Functional%20Chains%20and%20Physical%20Paths.mediawiki) describes connecting involved functions inside referenced chains, including functions that are not at a chain boundary. It also describes connecting two involvements of the same function, nested composite chains, and promoting interior link anchors to a folded chain's visible container. Its sequencing section distinguishes contextual precedence from functional-exchange involvement. This supports contextual endpoint addressing and distinct connection purposes. It does **not** establish that common-function assembly merges two executions into one; no such runtime meaning should be invented from the class name or illustration. + +Current Studio evidence: + +- `docs/language.rst:497–534`: distinct occurrence identity, reference reuse, same-owner direct SequenceLinks, explicit topology, and separate recursive-reference rejection. +- `model.rs:528`: `SequenceReference` contains only a sequence identity; `SequenceLink` endpoints are plain occurrence IDs. No caller/reference-instance path accompanies an interior endpoint. +- `validation.rs:2321–2369`: endpoints must have the same immediate Process/FunctionalChain parent; work/exchange adjacency also checks semantic endpoint correspondence. +- `tests/release_topology.rs:240`: direct foreign-owner links are intentionally refused by both `can_connect` and command execution. +- `behavior/topology.rs:238–252` and `topology/direct.rs:383`: the graphical contribution and connection predicates share those restrictions. Expanded reuse content is a source preview (`topology.rs:858–886`, `1140–1185`), not a caller-specific endpoint surface. +- `harness/release-process-editing.md:185–188` already records cross-reference inner-chain linking as unresolved. The recent behavior-transition navigation fix does not alter this boundary. + +## Counterexample and attempted substitutes + +Shared procedure P contains distinct contributions `a → b → c`, involving work A, B and C. Outer process O owns two references `r1 → P` and `r2 → P`, plus contribution x involving X. In O only, X supplies a condition/input required by **b inside r1**; a inside r1 need not wait for X. r2 remains unaffected. P must remain independently reusable, and later edits to P must be reflected through both references. + +The required target is the address `(O, [r1], b)`. `(O, [r2], b)` is a different contextual contribution even though it has the same leaf occurrence and engineering-work identity. The stronger nested case `(O, [r1, nested_reference], b)` prevents a one-level workaround from silently becoming the contract. + +| Attempt with current semantics | Why it does not preserve the stated facts | +| --- | --- | +| Link x directly to b | Rejected by the same-parent invariant. Merely relaxing that check leaves r1 versus r2 unspecified. | +| Link x to r1 | Identifies the reference as a whole, not its interior b. It cannot assert the desired interior connection. Treating it as a call-entry predecessor also introduces an unwanted dependency on a; the current specification does not define a special selective-entry interpretation that would fix this. | +| Add outer contribution b2 involving B and link x to b2 | b2 is a new occurrence. Shared work identity does not make it the b occurrence inside r1, and there is no typed binding identifying them. | +| Put x/the connection in P | Changes the shared procedure for every reuse and its standalone use, contradicting caller-local scope. | +| Split P into smaller procedures or flatten it into O | Can approximate selected traces, but changes the reusable procedure boundary or duplicates contextual topology. Subsequent shared edits do not preserve the originally requested single P contract. This is a model redesign, not an equivalent encoding of the same model. | +| Add a Constraint with prose naming r1 and b | Retains explanatory text, but supplies neither a resolved endpoint nor graph validation, projection or typed transition/review behavior. It cannot satisfy graphical/source/API parity for the relationship. | + +A second counterexample uses r1(P) and s1(Q), each with an interior contribution involving B. An explicit common-work connection must identify those two contextual contributions without fabricating a B→B global exchange. Two ordinary work occurrences sharing B do not express that assembly relation by themselves. Keep the precise common-work meaning at the modeling level until a stronger formal interpretation is justified. + +## Smallest coherent approach + +Prefer one shared **contextual occurrence address**: an ordered vector of SequenceReference occurrence IDs, followed by the leaf occurrence ID, resolved from an explicit owning Process/FunctionalChain. The empty vector means a direct contribution. Address by reference occurrence, never by referenced chain ID or work ID: both can repeat. + +Keep existing direct SequenceLink records and their old serialized meaning unchanged. Add a typed caller-owned contextual link record for the missing relationships, with source/target addresses and an explicit purpose: precedence with guard, assembly through an existing same-layer Interaction/FunctionalExchange, or assembly through existing common work. The exact public names are a design decision; a closed purpose enum avoids conflating a shared-work connection with an invented global exchange or silently treating every assembly edge as execution precedence. Both old and new records should resolve into one internal topology-edge representation used by validation, authoring and rendering. A disconnected new renderer or an unvalidated JSON field is not a solution. + +For exchange assembly, resolve both leaves to Action contributions and require their referenced work to equal the exchange's source/target. For common-work assembly, both resolved Action contributions must reference exactly the same existing work. A precedence link may use the applicable work/control/exchange contribution kinds; its graph rules must remain distinct from assembly incidence. Do not automatically identify or collapse separate occurrences merely because they share work. + +This adds a real persistent contract. New fields/variants must have explicit source/interchange admission and migration policy; do not loosen frozen source 1–6 admission to smuggle them into old versions. An initial implementation can avoid replacing the existing direct-link DTO while sharing its resolved topology machinery. + +### Occurrence-context invariants + +1. The owner exists and is the correct same-layer Process/FunctionalChain kind. Every path component is a direct SequenceReference of the current resolved owner; its referenced sequence becomes the next owner. The leaf belongs directly to the final owner and is a valid contribution. Missing, mistyped, wrong-layer, skipped and stale components reject with a located endpoint error. +2. Full addresses determine identity and equality. Repeating the same referenced chain or work elsewhere never aliases its occurrences. A self-link check compares full addresses, not just leaf IDs. +3. Reuse remains acyclic. Contextual precedence may still express the permitted topology loops; those are a different graph. Define one bounded, shared contextual graph/index so nested repeated reuse cannot cause uncontrolled expansion in every render or edge check. +4. Caller-owned links cannot mutate source-chain containment or its links. Changing a reference target must invalidate incompatible paths rather than redirect them by name or leaf UUID coincidence. Deletion/reidentification must visit every path component and leaf; deleting a component must never turn a path into an empty/direct path. +5. Fold/expand changes anchors only. The complete authored address remains in the link, source and inspector. Each expanded reference exposes the contribution in its caller context; Open source remains a distinct guarded action. + +## Alternatives and tradeoffs + +An exposed-gate/port model can be coherent if gates map explicitly to these same interior addresses. It is useful for deliberate public procedure interfaces, but requires extra declarations and caller bindings and cannot fully cover arbitrary interior assembly without eventual contextual resolution. It is a larger initial authoring burden for this task. + +Changing SequenceLink directly to addressed endpoints may reduce the number of record variants, but touches every source/target-ID consumer and requires a careful distinction between precedence and common-work/exchange assembly. A compatibility adapter plus the new contextual record is the smaller initial migration surface. Either representation must use the same semantic resolver; choosing the record spelling cannot avoid that work. + +Flattening with generated provenance still duplicates presentation instances or semantic records and needs synchronization/conflict rules. A global cross-owner link without a caller path is smaller code but wrong for repeated reuse. Neither is recommended. + +## Bounded acceptance and integration consequences + +| Gate | Required observable result | +| --- | --- | +| Core falsifier | Both OA and SA represent the r1-only interior connection; r2 and the complete shared P record remain unchanged. Repeat with two nesting levels and repeated work occurrences inside P. | +| Common-work case | Typed link identifies the exact two contextual B contributions; different-work candidates reject; no new global work/exchange is created and no unsupported execution-merge claim is made. | +| Lifecycle | Reidentify each path component/leaf, retarget a reference, delete an interior contribution and restore Undo. Invalid paths reject atomically; no widening, name lookup or accidental r2 redirection. | +| Graphical authoring | Expand r1, draw/select/reconnect its interior connection, compare r2, fold and re-expand with exact address retained. Picker/inspector show the caller/reference breadcrumb. Pending forms and stale gestures remain guarded. | +| Text and REST | Canonical source/JSON round-trip and schema admission; shared command/source updater; real HTTP creation/update/invalid-address rejection with unchanged revision and source on refusal. Publish OpenAPI examples for both empty and nested paths and enforce declared address/expansion limits. | +| OA↔SA/review | Required/Subtree closure includes the owner, each reference occurrence, referenced chain, leaf and selected assembly fact. Map every typed path component; preserve raw drafts and exact addressed facet review, source mutation and single Undo/Redo. Never map by display name or assume one occurrence per work. | +| Coverage/presentation | Audit core reachability, source references/deletion, transitions, semantic exact slots, topology scene/context handling and selected behavior navigation against the shared resolver. Coverage must not confuse one caller's contextual edge with a global modification to P. | + +The implementation should be accepted on these finite engineering cases rather than an open-ended “all Capella” checklist. Ordinary direct links and existing procedure reuse must retain their current behavior. A new schema, an endpoint picker alone, or successful compilation alone would not close this release gap. diff --git a/harness/release-chain-context-analysis-contract-20260906.md b/harness/release-chain-context-analysis-contract-20260906.md new file mode 100644 index 00000000..f124db25 --- /dev/null +++ b/harness/release-chain-context-analysis-contract-20260906.md @@ -0,0 +1,141 @@ +# S1 contextual analysis projection — proposed final rule, 2026-09-06 + +Read-only design against current `validation.rs::validate_sequence` and +`validate_topology`, the updated S1 contract and its resolver probe. No production +edit, Cargo, simulation or native input. Address, constructor, deletion and +cache-independent admission choices are already ratified; this note resolves the +remaining diagnostic projection. + +## Decision for X → R1.B + +**Yes: emit a caller-qualified advisory `ambiguous_sequence_branch` at +`(O, [R1], B)` when the resolved authored Precedence inputs are A→B and X→B.** +It is a non-control contribution with two incoming precedence links, matching the +current structural heuristic. Do not suppress it because one edge crosses a reuse +boundary. Do not turn it into an error, silently create a Join, edit P, or declare +an execution ambiguous as a proven fact. In particular, the warning does not mean +that X must precede A or that either dependency may be ignored. + +Use wording such as: “This contribution has multiple incoming precedence links in +this caller context. Review whether explicit merge/join control is needed to +describe the intended paths.” The current generic alternative/parallel wording is +too strong if presented as a theorem about a caller's additional constraint. The +engine checks graph shape; it does not evaluate guards or model token flow. + +Standalone P remains A→B→C with no new warning, and R2.B has one incoming link. +The qualified warning belongs to caller O, with breadcrumb R1/B and the two link +identities. It must not appear as an unqualified defect on B's shared definition. +The engineer may retain the advisory if independent precedence constraints are +intentional; resolving it is not a prerequisite for saving this valid relationship. + +This preserves Studio's existing non-control fan-in/fan-out advisory convention. +It is a **design choice**, not a claim that Capella mandates the same warning. +The official guide establishes precedence/conditions and distinguishes assembly +incidence, but does not specify Studio's completeness algorithm. [Primary guide, +Sequencing and Control / Assembling Functional Chains](https://raw.githubusercontent.com/eclipse-capella/capella/master/doc/plugins/org.polarsys.capella.ui.doc/html/05.%20Diagram%20Management/5.5.%20Creation%20of%20Functional%20Chains%20and%20Physical%20Paths.mediawiki). + +## Three deterministic projections + +1. **Precedence:** existing SequenceLink plus ContextualLink::Precedence. Keep + distinct authored link IDs, guard text and qualified endpoints. Existing + control-cardinality, missing-guard and non-control degree rules inspect these + edges, never assembly incidence. Distinct authored parallel links still count + separately as today; memoization must not collapse different guards/link IDs. +2. **Assembly incidence:** ContextualLink::Exchange and CommonWork retain typed + source/target addresses and the engineering exchange/work identity. They are + available for drawing, selection and connectivity inspection, but add no + precedence arc, control branch, execution merge or unique-entry claim. Do not + introduce a new assembly-completeness ruleset in this slice. +3. **Coverage:** resolved Action/Exchange occurrences and references retain their + existing involvement meaning. An Exchange-purpose link is explicit involvement + of its selected Interaction/FunctionalExchange in its **caller**; count it as + that context's exchange evidence. CommonWork points to work already identified + by its Action endpoints and does not count two copies of a work definition. + Precedence alone never invents a global exchange. Do not mark standalone P as + covering a caller-only exchange merely because its B is an endpoint. + +All are derived views of one immutable accepted Project and its validated address +resolver. Purpose switches change only the corresponding projections, followed by +ordinary whole-candidate validation. Analysis output must be independent of cache, +traversal order and the renderer's expanded/folded state. + +## Repeated and nested qualification + +Analyze one root caller O. Every occurrence instance has key `(O, path, leaf)`. +For each reference occurrence R in a resolved sequence instance with prefix path q, +its callee's contributions use prefix `q ++ [R]`. Qualify that callee's ordinary +links by the same prefix. Qualify a contextual link **defined in the callee** by +prepending q to both of its local paths; a contextual link defined in O appears +once with its own exact paths. Preserve both definition relation ID and instance +prefix in derived edge identity/provenance. + +Thus P's A→B exists as R1.A→R1.B and R2.A→R2.B, while O's X→R1.B appears only +once. If P itself references Q through N, the address is `[R1,N]`, not `[P,Q]`. +Expanding one preview or following Open source cannot change these identities. + +## Reference boundary expansion without invented scheduling + +Use ephemeral **entry/exit boundary vertices** for each reference instance solely +for graph connectivity; they are not semantic contributions, execution records, +stored IDs, guard-bearing links, or controls. A caller link targeting a reference +attaches to its entry boundary; one sourced at the reference attaches to its exit +boundary. A link targeting an explicit interior address attaches directly to that +contribution and does not acquire a dependency on its earlier contributions. + +Compute a callee instance's boundary candidates from its own local precedence +projection **before the containing caller's links are attached**. Include contextual +Precedence defined in that callee, qualified to this instance. Connect the entry +boundary to its structurally zero-incoming contributions, and its zero-outgoing +contributions to the exit boundary using marked synthetic arcs. Nested references +use the same rule recursively with memoized shape, within the ratified limits. +Never choose the first list element as a hidden fallback. Preserve established +legacy exchange-only list order when expanding a legacy sequence; this does not +silently convert it to saved SequenceLinks. + +Synthetic boundary arcs support reachability only. Exclude them from authored +degree counts, control cardinalities and missing-guard checks. A reference's own +authored incoming/outgoing degrees are counted on its entry/exit boundary and +reported against that reference occurrence, preserving existing direct-reference +diagnostics. Do not count both the collapsed reference and its inner contributions +as independent work executions. + +Multiple intrinsic entry/exit candidates remain advisory incompleteness; boundary +arcs do not assert AND versus OR synchronization. Missing reference, empty callee, +or a callee with no structural entry/exit supplies no proven pass-through path: +retain its located incompleteness warning and mark dependent caller connectivity +analysis incomplete. **Do not add entry→exit bypass**, nor infer nontermination +from an incomplete subgraph. Structural address validation is still exact. + +## Small evaluator oracles + +All cases below use the same P=A→B→C and references R1(P), R2(P), with no implicit +ordering between R1/R2/X unless stated. These are algorithm expectations for future +tests, not newly executed results. + +| Case | Exact expected distinction | +|---|---| +| R1, R2 only | Instances remain distinct; each B has one authored predecessor. Root has two independent precedence entries/exits and may retain the existing start/end advisory. Reuse itself adds no non-control degree warning. | +| Add X→R1.B Precedence | R1.B has two authored inputs and one output: one qualified branch advisory. R2.B and standalone P unchanged. Root starts are R1 entry, R2 entry and X; root exits are the two reference exits. X has a path to R1 exit through B/C. No edge X→R1.A exists. | +| Replace with X→R1 Precedence | Caller edge ends at R1 entry, so A lies downstream of X. This is observably different from the interior-target case; B has only A as its authored predecessor. | +| Add X→R1.B Exchange-purpose instead | Adds exchange incidence and caller exchange coverage, but not a second precedence input to B. Any unsequenced X/start/end warning remains honest precedence-only feedback; it must not say that no assembly connection exists. | +| Add R1.B↔R2.B CommonWork | Two exact contextual incidences; no B→B global exchange, no execution fusion and no precedence path between instances. | +| Use target [R1,N].B | Only that nested instance receives the input; [R2,N].B and any other reference to Q remain untouched. | +| Callee entry Choice already lacks required input | Its intrinsic warning is not “repaired” merely by a synthetic entry arc. A real authored contextual input can change the caller instance's degree, with qualified provenance, while standalone definition diagnostics remain separately visible. | +| Missing/empty/budget-exceeded callee expansion | No complete-looking reachability result. Exact structural failures reject; analysis-limit/incomplete-reference notices prevent conclusions from a partial expansion. No guessed bypass or unreachable/nonterminating cascade. | + +## Diagnostic location and release bounds + +Existing Issue fields can locate the root owner or authored contextual link and +include a breadcrumb in the message, but unambiguous graphical navigation requires +preserving the full owner/path/leaf provenance in an internal diagnostic target. +If exposing that target in REST/source diagnostics, add a closed optional context +record rather than overloading the leaf ID. Do not attribute the same qualified +warning several times through folded and expanded renderings. + +Do not replace historical direct-only diagnostics merely to normalize counts. +Use the contextual evaluator for affected caller contexts and test existing +SequenceLink-only fixtures unchanged. Start/end/reachability checks remain +conservative structural assistance, not scheduling, liveness proof, execution +counts or guard satisfiability. Tests should assert qualified warning locations, +purpose-specific coverage and exact no-cross-instance effects rather than one +aggregate number of warnings for an arbitrarily incomplete whole fixture. diff --git a/harness/release-checkpoint-integration-review-20260906.md b/harness/release-checkpoint-integration-review-20260906.md new file mode 100644 index 00000000..54bcc414 --- /dev/null +++ b/harness/release-checkpoint-integration-review-20260906.md @@ -0,0 +1,27 @@ +# Current staged checkpoint: bounded independent integration review + +Reviewed the staged checkpoint on 2026-09-06, observing 287 changed files. This is a consequential integration, reproducibility and accidental-disclosure review, not a claim to have reviewed every changed line or accepted the release objective. No production files were changed and no Cargo or native tests were run for this review. + +## Findings + +No material defect found within the checks below. + +| Check | Observed result | +|---|---| +| Staged fixture inventory integrity | All **65** entries in the five inventories below match SHA-256 of their exact staged Git blob bytes, including inventoried generators and READMEs. No stale digest was found. | +| Byte-preserving checkout/staging | All **145** tracked files beneath the three explicitly protected path families have identical staged and working-tree bytes. `.gitattributes` disables text normalization for these exact source/recovery/history oracles. | +| Packaged teaching inputs | All **30** current teaching `.arcadia` files are tracked. `scripts/package-windows.ps1:125` enumerates tracked `docs`, `examples` and `deployment` inputs and therefore includes the complete teaching folder, including its evidence files. This checks input reachability; it does not rerun packaging. | +| Staged local/runtime data | The staged filename inventory contains no `.local`, build target, runtime executable/DLL, credential/key or environment-secret files. The source-5 recovery JSON is deliberately synthetic: its adjacent generator uses fixed test identities, `origin: None`, fabricated source and invalid raw drafts. | +| Credential-pattern scan | Zero matches in staged added lines for private-key headers, AWS access keys, GitHub/OpenAI token patterns or credential-bearing URLs. This bounded pattern scan cannot establish the absence of every possible secret. | +| Version boundary | Staged `crates/oa-core/src/model.rs` still declares schema **6**. The contextual-chain and interface-operation material is design/probe evidence; source/schema 7 is not implemented by this checkpoint. | +| Platform dependency boundary | The new direct `libc` dependency is scoped to Linux in `oa-core/Cargo.toml`; it does not introduce a Windows runtime dependency. Runtime correctness remains covered by the separate platform evidence, not by this manifest inspection. | + +The checked inventories are `harness/fixtures/interface-operation-probe/hashes.json`, `harness/fixtures/release1-native/artifacts/semantic-artifacts.hashes.json`, `harness/fixtures/release1-native/communication-means/hashes.json`, `harness/fixtures/release1-native/semantic-attachments.hashes.json`, and `harness/fixtures/release1-native/semantic-batch.hashes.json`. The attachments inventory retains original absolute task-local paths as provenance; its tracked files were resolved by their unique basenames in the inventory directory and hashed from the index. No runtime data at those original paths was staged. + +The byte-protected path families are `harness/fixtures/release1-native/**`, `harness/fixtures/interface-operation-probe/**`, and `harness/history/2026-09-06-ledgers-before-consolidation/**`. The existing exact upstream license whitespace must likewise remain unchanged; whitespace cleanup is not a substitute for a byte oracle. + +## Reproduction and limits + +Enumerate the candidate with `git diff --cached --name-only`. Hash the bytes emitted by `git cat-file blob :` with SHA-256 and compare each manifest entry. For the protected path families, compare `git hash-object --no-filters -- ` against object IDs from `git ls-files --stage -- `; all 145 matched in this review. The ignored detailed hash result is `.local/checkpoint-hash-audit-current.json`. + +Root separately reports the latest strict Clippy pass (28.26 seconds); this audit neither reran that command nor extends its result to subsequent changes. Native verification of the newest semantic-review navigation binary remains a separate gate. Source-7 contract decisions and implementation, and the remaining objective gates recorded in the current ledger, remain unfinished. A reproducible checkpoint is not a feature-complete release claim. diff --git a/harness/release-communication-native-20260906.md b/harness/release-communication-native-20260906.md new file mode 100644 index 00000000..873fd081 --- /dev/null +++ b/harness/release-communication-native-20260906.md @@ -0,0 +1,69 @@ +# Communication means native review — 2026-09-06 + +Goal remains active. Current corrected reverse-scenario native gate **PASS** on desktop `F5ECAEC02BBAFBC6F3BDC6ADEB746AFE91F3A495C6D9564A68DEA41E61AC9246`: Behavior context, unchanged architecture view, exact three-file Save/Undo/Redo and reopen. See the final section; earlier trials and their binary-specific limitations are retained below. Tracked fixtures/oracles: fixtures/release1-native/communication-means/README.md. + +Initial draw/create/reuse setup: Windows process9800, GUI SHA-256 A5A741349B49D0C90A6BDB0FA7871415133CBCC1B8A4ECF0C4EDDDB99313F2D2, isolated bin-source6-guide/profile and system-only PATH. + +## Draw and explicit create-port transition + +At1100x700/66% zoom opened cm-draw.arcadia. Both participant names and both activity names were readable. Communication means highlighted Rescue team/Control centre and not activities. Clicked Rescue team then Control centre; created one participant relation. Properties renamed it Rescue radio channel. Saved isolated baseline .local/native-qa/cm-create-before/entry.arcadia on first Save submission. Source contains new identity3e576659-3455-4fb5-811e-5f4a326948f4, original generated alias communication_means_rescue_team_control_centre, empty description/items, oriented false, source cm10/target cm11. Independent drawn-baseline whole-model/source check is separate/pending. + +Initial fixture instructions incorrectly said choose Selected only before participant mapping. That hid the dependency rows and left only create-port choices. Cancel returned clean accepted baseline. Reopened Required references, explicitly confirmed Rescue team→Rescue radio and Control centre→Control radio. Source reused Existing team socket; target selected Create Standard port · bidirectional. Missing either endpoint kept Apply blocked. Retained both target Names when prose conflicts appeared, then narrowed to Selected only: both endpoint decisions retained and Apply enabled. One Apply created a boundary exchange and one new port. Native SA view showed the connected existing team socket, two Control radio sockets, and named new target port parent Control radio. Existing control socket remained; no interface/allocation action was taken. + +Saved .local/native-qa/cm-create-after/entry.arcadia. Independent check create passed full Project and exact complete reachable source with two generated UUID substitutions only; no placement identities. Preserved accepted two-file bundle at .local/native-qa/cm-create-accepted. One Undo then Save matched baseline entry byte-for-byte, SHA25640363373B2443714F38D5825F8FAF53183CF004962059E0B611D01DB8E7F7F7E. The no-longer-included generated traceability file remains on disk under the non-destructive save policy; it is not reachable from the restored entry. One Redo then Save restored both accepted files byte-exact, retaining UUIDs: + +- entry.arcadia: FC790673723FE81D049CBCA3EE36A5BDDB09F534D906C18BF3355D72EF23A5C1 +- traceability.arcadia: ACAE3DAD95CD3F91A2D4E9FF8D64D7E9BA27888514B2C9DC5793B6941D7B5485 + +All three transition Save submissions succeeded first attempt (plus baseline Save). GUI clean afterward, stderr length0, no recovery checkpoints; only recent-models.json present. Isolated native SaveAs type_text once left filename empty before submission; reobserved/refocused/reentered successfully. This input-helper observation is not an application save failure. + +Both-existing-port reuse, reverse scenario Call/Reply, default-size layout, directional Flow refusal and crash recovery are separate gates; this result does not claim them. Revised reverse oracle predicts GUI automatic destination placements independently and rejects changed geometry/metadata/element/source bytes. + +Independent draw baseline check subsequently PASS: typed CreateRelation using initial generated engineering name and alias, then name-only EditRelation, with production source updater after each operation. Actual model supplied only the new UUID. Complete Project and full source equal the saved native baseline; expected entry SHA25640363373B2443714F38D5825F8FAF53183CF004962059E0B611D01DB8E7F7F7E. Log .local/native-fixtures/cm-native-draw-check.log, independent expected bundle/model/exact commands .local/native-fixtures/cm-native-draw-oracle. Orientation/alias counterfactuals rejected by full-model equality; added whitespace rejected by exact-source equality. This closes the separately pending drawn-baseline oracle above. + +## Both existing ports native alternative + +Reopened the exact preserved before entry in the same process/minimum size. Through channel context action, confirmed both participant mappings under Required references, narrowed to Selected only, then explicitly chose Existing team socket and Existing control socket. Both choices remained available in the narrowed scope. No Name reconciliation was required because participant updates were outside the final scope; Apply remained blocked with either endpoint absent and enabled once both were chosen. One Apply showed the boundary exchange connected to the two existing sockets; object count stayed8. + +Saved .local/native-qa/cm-reuse-after/entry.arcadia. Independent check reuse PASS full Project/exact complete reachable source with2generated UUID substitutions only,0placements; no new port. Preserved accepted bundle in cm-reuse-accepted. One Undo/Save restored baseline entry exactly (40363373B2443714F38D5825F8FAF53183CF004962059E0B611D01DB8E7F7F7E). One Redo/Save restored both accepted files byte-exact: + +- entry.arcadia AE9CDFB1CAAF4EAA8CCFCF9E1603D7CBB2429649B28E827ADAF8190A03E4E08A +- traceability.arcadia 20FA451694101A9060BF2D7F2A6A9F4510C1BA4AC20DF9F363CCB363D3255DEF + +All3Save submissions succeeded first attempt, cleanGUI/0stderr/0recoveryJSON. Across create/reuse alternatives6transition saves pass plus drawn baseline Save. Reverse scenario and other previously listed gates remain separate. + +## Legacy reverse scenario: semantic pass, architecture navigation defect + +Process4288, default1440×900, walkthrough GUI SHA256246BACE21E254985EE90EF05132139DD7D6AF75085DE9E58FEB49477A493407D. Opened tracked cm-reverse-scenario.arcadia. SA call ran Control radio→Team radio; dashed Reply ran Team→Control and correlated to Request rescue report. Whole subtree SA→OA confirmed participant mappings to Rescue team/Control centre and boundary mapping to Rescue radio channel, retaining both OA participant Names. + +One Apply incorrectly navigated to Architecture, selected the first reused participant instead of the requested scenario, and placed five behavior elements into the existing communication view. Their overlap obscured participant/activity labels and caused three route warnings. This was a material UX failure on this binary. Semantic success below did not close that finding; the corrected native trial is recorded separately below. + +Saved .local/native-qa/cm-reverse-after/entry.arcadia. Legacy independent check reverse PASS complete Project and exact source with15generated UUID substitutions, including5placement identities independently predicted by the old desktop placement policy. Accepted copy retained at cm-reverse-accepted. One Undo/Save restored tracked baseline entry exactly, SHA2561ABC4C4CAC37CFB06ED3B12A115F9E7E6A2D3AFFD34B25C355309E5EE465FA40. One Redo/Save restored all three accepted reachable files exactly: + +- entry.arcadia F407ED3CA6C34F096B4B0E3463EA0FC9CD2BE75F84C104009CD86EE361BED629 +- traceability.arcadia 076CFF57F4A5EFFD5CE99B5008432C2C6ADDC499A8AAE2595C4DEB534AB463A6 +- oa/behavior/scenario_oa_cm_200.arcadia E187EDDDD3C814D7DF4879F0C96D784BAB8D7A9F11E1BFECD731F4D1E10364BC + +The original report listed only the entry and traceability hashes, but its initial complete-source oracle covered all three files. The third module was subsequently hash-compared after Redo against the preserved accepted copy and passed; its final on-disk hash was independently re-read for this correction. + +All three Save submissions succeeded first attempt; filename input required refocusing before submission (one unsupported UIA set_value attempt reported CacheRequest failure without editing the field). Clean GUI, stderr0bytes, no recovery checkpoint. Undo leaves the unreachable generated trace file on disk under the non-destructive save policy. + +Manually opened Behavior afterward. OA participant lifelines visibly bind to Rescue team and Control centre, preserving solid leftward call and dashed rightward reply. Selected reply @oa_cm_204 inspector shows exchange Rescue radio channel @cm_50, Reply, receiver @oa_cm_202, correlated call @oa_cm_203, sender @oa_cm_201. Full-model oracle verifies no OA ports were introduced. Native reverse semantics/history passed on this legacy binary; automatic destination navigation and architecture preservation were then revalidated in the corrected trial below. + +## Corrected reverse scenario: Behavior context and architecture preservation PASS + +Process9672, default1440×900, desktop SHA-256 F5ECAEC02BBAFBC6F3BDC6ADEB746AFE91F3A495C6D9564A68DEA41E61AC9246, stage `.local/native-qa/bin-source6-behavior-context`. Root repeated the same preserved `cm-reverse-scenario.arcadia` baseline. Whole subtree confirmed both participants and the boundary, retaining both OA Names. Apply immediately stayed in Behavior, selected `@oa_cm_200`, and displayed the mapped participant lifelines with a solid leftward Call and dashed rightward Reply. Manual Architecture navigation showed the original four readable objects, no overlap and no route warnings. + +Saved `.local/native-qa/cm-reverse-behavior-after/entry.arcadia`. Independent `check reverse-behavior` PASS: complete Project and exact complete reachable source, ten generated UUID substitutions, zero placement identities. Accepted copy is `cm-reverse-behavior-accepted`. The separately retained legacy checker still expects fifteen UUIDs/five placements; neither oracle's semantic or source equality was weakened. + +One Undo/Save restored the baseline entry exactly, SHA-256 `1ABC4C4CAC37CFB06ED3B12A115F9E7E6A2D3AFFD34B25C355309E5EE465FA40`. One Redo/Save restored all **three reachable files** exactly: + +| File | SHA-256 | +| --- | --- | +| `entry.arcadia` | `EC826B9B84616507490AC30E4D57100DD6338DBA2F7071E879A8FB58820A85C5` | +| `traceability.arcadia` | `A56BFEFBCA3CE2259E5EB3B374A4BF6BDD9E2F23442ADEA5892791993997F0FC` | +| `oa/behavior/scenario_oa_cm_200.arcadia` | `87DF2C4A62118C326FC0B3A5768075754B643B05EDBC596C0DB5F81A903EC42A` | + +These final hashes were independently re-read for this report. Root reopened through Home recents and observed the original architecture and mapped scenario cleanly. All three Save submissions succeeded first attempt, with zero stderr and zero recovery checkpoints. Initial Save As filename focus/Ctrl+A/type worked on the first attempt. + +This closes the reported reverse-scenario navigation defect on the corrected binary. Ordinary architecture transitions and explicit destination requests remain covered by focused automated tests, not a separate native pass in this trial. Directional Flow refusal and communication-specific crash recovery remain distinct from this result. diff --git a/harness/release-completeness-audit-20260906.md b/harness/release-completeness-audit-20260906.md new file mode 100644 index 00000000..0e10837c --- /dev/null +++ b/harness/release-completeness-audit-20260906.md @@ -0,0 +1,51 @@ +# Independent release-completeness audit — 2026-09-06 + +Read-only requirements/code review of the working release tree after source6, artifact review and Linux journal integration. Authority: the supplied `10b0f701-1743-4de7-9ee1-225920e19649/goal-objective.md`, `docs/language.rst`, `docs/scope.md`, `harness/PLAN.md`, selected `DECISIONS.md` contracts and current evidence. No production changes, Cargo, VM launch or native input were performed. The older coverage audit is a requirements map; its statements that communication means, selective views and review controls are absent are historical, not current findings. + +**Conclusion:** this review did not establish another missing OA/SA core concept in the selected R1–R6/source6 profile. It did find an unfinished required teaching/example deliverable and a narrower behavior-export gap. Neither the implemented catalog nor this inspection proves full release acceptance. Current platform/runtime/package gates remain real obligations; they must not be recast as optional limitations. + +## Requirement → realization → remaining gap + +| Requirement | Current implementation and bounded evidence | Finding | +|---|---|---| +| OA responsibility; SA system boundary; meaningful nesting | `model.rs`, `validation.rs`, `projection.rs`, `ports.rs`; separate work ownership, leaf allocation, roles and one SA System. Prior architecture/port native records. | No new absence found. Internal logical/physical components belong to the explicitly deferred layers. | +| Missions and reusable capabilities | Typed include/extend/generalization, mission involvement and exploitation in `model.rs:847`, validation cycle/condition rules and `workflow.rs`. | Earlier R2 absence is resolved. Finish the integrated engineering example rather than add duplicate concepts. | +| Ordinary OA participant communication | `RelationBody::CommunicationMean`, allocation validation, pure `resolve_message_endpoints` (`validation.rs:1294`), explicit SA port decisions in `transitions/communication.rs`; source6 core/HTTP tests. | Implemented. Current native CM/transition proof is root-owned work, not missing semantics. | +| Contextual operational processes / functional chains | Occurrence Work/Control/SequenceUse plus SequenceLink; `behavior/process.rs`, direct canvas events and shared validation. | Branch/reuse/control semantics are present. Derived layout is an explicit design choice; historical static-view criticism does not describe this implementation. | +| Scenarios with reusable, typed interactions | `OccurrenceBody`, explicit viewpoints, correlated Call/Reply, qualified values and bindings; `behavior/scenario.rs` and editable `Scene`. | No new absence found. Free lifeline-column positioning is deliberately outside the current persisted scenario layout (`release-scenario-editing.md:12–20`); do not mislabel it missing message/reorder authoring. | +| State/mode behavior and typed events/actions | `state_validation.rs`, `behavior/state_canvas.rs`, `state_edit.rs`, `state_view.rs`; distinct pseudostates, nested regions, internal/local/external transitions and maintained action links. | Core and editing exist. Faithful behavior-image export is incomplete; see F2. | +| General-system data/interface semantics | `data.rs`, `ports.rs`, `forms/data_fields.rs`, `data_values.rs`; bounds/defaults, physical nature vs mechanism, record/reference storage, qualified fields, interfaces and protocols. | No new required type-family absence found. Executable expressions, object-instance defaults and a units conversion engine were explicitly excluded in the selected data contract; their absence is not a newly discovered blocker. | +| Bidirectional, iterative semantic reconciliation | `transitions.rs`, closed `semantic/*`, desktop `semantic_review/*`; scalar/constructor/keyed/order, Child/Incident, artifact lifecycle and combined requests. Core absence oracle, GUI tests and recent exact source/history native records. | Former prose-only and missing attachment/artifact/multi-source claims are obsolete. Remaining native cases and corrected diagnostic wording are acceptance work, not absent adapters. | +| Saved readable views | `view_options.rs`, projection/reference lifecycle, `forms/view_fields.rs`, duplication command and diagram canvas; category/coverage/context/hide/label settings. Forty-exchange native fixture and pure/HTTP evidence. | Implemented; grouping was conditional on failure of the dense-view gate, which did not occur. Do not add artificial summary exchanges as an ungrounded requirement. | +| Actionable completeness checks | `validation/coverage.rs`, shared behavior evidence, source locations and REST diagnostics. `release-rest-coverage-verification-20260906.md`. | Non-leaf exchange and behavior/mission/capability warnings are implemented. Do not reopen the old task3 absence claim. | +| Equivalent graphical/textual models, modular Git-friendly source | Normative source6, parser/updater, references, frozen older admission, typed forms, source workspace/lint/highlighting and coupled Document history. Exact Save-as and source5 checkpoint/native oracles. | No newly proven missing persistent-field editor in this bounded inspection. A closed core catalog alone does not prove all GUI parity; final-tree cross-representation acceptance remains required. | +| Arbitrary artifact traceability and deployable automation | Artifact metadata and owner lifecycle; shared REST commands/queries, source-bundle envelopes, ETags and hosted desktop adapter. Actual HTTP48 and native hosted evidence. | Opaque references intentionally do not execute/fetch CAD/code/documents. Plugin ABI, replicated collaboration and remote deployment execution are not implied first-release requirements. Linux full application verification remains open. | +| Intuitive onboarding, guide and worked engineering scenarios | Home/recents, searchable Rust guide, recovery, source diagnostics; Home opens the rescue demo. Existing teaching examples contain useful OA/SA/ports/state/artifact work. | **F1: the expanded first-release worked project is not shipped.** Current examples do not teach the newly required source6 engineering chain. | + +## Actionable implementation/deliverable gaps + +**F1 — Required release teaching project is missing (P1 deliverable, not a missing metamodel).** PLAN milestone2 requires a worked example for each addition, and milestone5 requires guided OA goals/roles/process alternatives through SA contracts/chains/scenarios/states/evidence. The shipped `examples/coastal-rescue.arcadia`, `routing-workshop.arcadia` and `source-project/project.arcadia` still use `arcadia 3`; old version alone is not a defect, because these are valuable migration fixtures. The stronger finding is content: `examples/` and the Home demo builder `crates/oa-core/src/demo.rs` contain no CommunicationMean, ExchangeCategory/view options, SequenceLink/reusable process contribution, or capability include/extend example. Their useful demonstrations currently live in tests and `harness/fixtures`, not an ordinary user-facing model. Home's `Explore rescue example` (`home.rs:311`) cannot expose that missing content. + +Add a **separate** current-profile worked project and concise tutorial mapping. Preserve frozen legacy examples. It should connect one coherent OA mission/process and participant communication to explicit SA boundary-port/functional counterparts, an alternate/reused behavior path, a bounded data contract, state behavior and an artifact. Provide two saved views of the same identities and one deliberate nonblocking incompleteness warning. Reopening and one reviewed change should produce documented source/REST results. Root's active guide/CM work is the natural integration point; this audit does not assert that its in-progress changes are finished. + +**F2 — Shareable behavior diagrams are not implemented faithfully (P2 product/scope gap).** `diagram.rs:1547` exports the generic projected architecture view. Its node emitter always writes rounded rectangles with kind/name; it has no StateKind glyph or typed StateAction/internal-transition row handling. The state renderer instead provides these in `behavior/state_canvas.rs:326–413` and its state drawing functions. `behavior/state_view.rs:85–104` offers views/Fit/zoom but no export action; process/scenario `Scene` likewise has no vector export path. Thus the guide's general report-image wording (`guide.rs`, Files) is broader than the actual behavior output capability. This is an inspected path difference, not a native export failure reproduced here. + +The original objective does not independently mandate SVG for every scene, so this is **not automatically a release-blocking new requirement**. Root should explicitly settle the boundary: preferably reuse the existing scene/geometry semantics for faithful state/scenario/process export; otherwise clearly scope the current command to architecture views. Do not present a generic state rectangle as an equivalent exported state machine, and do not invent new semantic records to solve an output-adapter problem. + +## Three highest-value next gates + +1. **Complete and exercise the current-profile teaching project.** This closes F1 while testing several features together in an engineer's actual OA→SA→review workflow, rather than adding disconnected demos. Preserve existing migration inputs. +2. **Resolve behavior-output scope, then finish the end-to-end workflow matrix.** Close F2 with an explicit decision/implementation and validate the remaining root-owned CM, behavior, hosted and recovery cases on one traceable tree. Do not treat old executables or unrelated fixtures as current acceptance. +3. **Verify the complete Linux application and final distributable tree.** The recent Ubuntu/Rust1.90 core38 baseline, patched42 library tests and three executed syscall injections are valuable but do not compile or run the desktop/server. Follow the measured server-first plan in `release-linux-validation-options.md`, then actual Linux GUI; finish final locked suites, strict checks, rendered reST/OpenAPI, independent review and clean-environment packages/checksums. + +Accepted non-goals remain LA/PA/deployment modeling, execution/simulation, native Capella file compatibility, full SysML conformance, semantic Git merging/LSP, plugin ABI, production multi-user synchronization and signed installers. These boundaries are supported by the objective's later-layer deferral and selected contracts; they do not excuse missing primary OA/SA workflows, user-facing examples or required Windows/Linux acceptance. + +## Follow-up decisions and implementation evidence + +The findings above describe the audited tree before this follow-up. F1 now has a separate source-6 project at `examples/release1/project.arcadia`, with a guided engineering walkthrough and local evidence artifact in that folder. The shared Rust generator (`crates/oa-core/examples/generate_release1.rs`, `examples/release1/model.rs`) executes the real OA→SA planner with explicit port decisions, then completes the interface/port allocations and saved views. It emits 90 elements, 60 relations, 5 views, 65 traces and 28 source files. Existing legacy examples are untouched. + +`crates/oa-core/tests/release1_teaching.rs` passes **4/4**: deterministic shipped-source/interchange parity and the one deliberate located warning; Call/Reply direction, process/scenario reuse and stable filtered-view port geometry; missing port decisions and invalid performer atomic rejection; and a real reviewed Name change preserving unrelated modules. Generator, focused tests and strict Clippy passed under the current Windows gnullvm Rust1.98.1 toolchain, recorded in `.local/release1-teaching-{generator,tests,clippy}.log`. This closes the absent worked-project implementation finding. The Home/guide hook, packaging and native walkthrough are integrated/verified separately by their owners; these core checks do not claim their acceptance or a Linux/full-service run of this particular example. + +Root explicitly selected the narrow F2 boundary: current export is **Architecture SVG**; faithful process/scenario/state-scene SVG is deferred beyond this first-release export scope. The new tutorial states this boundary. F2 is therefore a scope/wording qualification, not an invented blocker or a claim that generic state rectangles reproduce the interactive behavior scene. Final guide/package wording remains part of root's release review. + +Independent teaching review then caught an overbroad assessment-capability context on the repair conversation. The corrected lesson scopes that conversation to restoration and gives assessment its own single-action procedure and ChainInvolvement, reused by the main process. This preserves assessment coverage without hiding extra warnings. Current generated counts are **94 elements, 62 relations, 5 views, 68 traces, 30 source files**; the single audit-function warning remains. Expanded four tests passed using a bounded direct `rustc -D warnings` compilation against the already-built Windows core (no new Cargo build), including both-layer context/procedure assertions and exact decoded equality of all shipped replay/diagnostic JSON. Logs: `.local/release1-teaching-corrected-generator.log` and `.local/release1-teaching-corrected-tests.log`. Subsequent integrated Cargo/native/package checks must use these corrected files. The contemporaneous Linux server gate deliberately retains its separately hashed earlier snapshot and does not imply verification of this later example correction. diff --git a/harness/release-current-native-verification.md b/harness/release-current-native-verification.md index 5195b740..2e52e7c0 100644 --- a/harness/release-current-native-verification.md +++ b/harness/release-current-native-verification.md @@ -1,5 +1,46 @@ # Current Windows lifecycle and keyed-review verification — 2026-09-06 +## Ordered review, retained rejection and exact source — current continuation + +The main OA→SA ordered fixture passed actual native editing on the corrected +binary660BECC8DB96B218D12805B39661B46C9F48DF4F8F07CB3B7D42C1F803BE089F, +then crash recovery and Apply/Save/Undo/Redo on reference-padding build +D402F9B09A454B7AB1239DAB9102E6E20107BCC38AFA29740B1837592E98A04E. +The latter build passed in .local/reference-padding-build.log (3m46s), staged +with libunwind.dll under .local/native-qa/bin-reference-padding, isolated appdata +and system-only PATH. Current native process8908/window853042; prior10896 stopped. + +At1100×700, explicitly paired Request start, Verify interlock and Energize in +Reference pairs. Taking Verify membership refused ambiguous ordered insertion. +Set final value for Steps order seeded Request, Local self-test, Energize, Verify; +one Up on Verify produced Request, Local self-test, Verify, Energize, zero impacts. +Leaving the Verify membership decision unselected refused the exact-survivor +contract while retaining that four-entry order. Checkpoint +41b850e8-faac-431c-b040-e016065452f6 is copied to +.local/native-qa/ordered-before-crash.json. Forced process stop and native Home +Recover restored all four reference pairs, the lone order Candidate, selected +member address and same error. Navigating back to final order showed all authored +positions plus “No longer included: SA Verify interlock”. Retaking Verify repaired +the preview without editing the order. Apply was one atomic transaction. + +Save As .local/native-qa/ordered-recovered-result.arcadia is byte-exact against +.local/native-fixtures/semantic-ordered.after-order.arcadia, SHA256 +65A6217F474B5085F37B511151185C2C08FF7A1998E924F1FE01F29C410F6CED. +One Ctrl+Z and Ctrl+S restored the initial fixture byte-exact, SHA256 +C59EC1233D5A9FBB8E386F6D9B13F398BE21BAF813BA215EFB51BFC1BB98DB36; +copy .local/native-qa/ordered-undo.arcadia. Ctrl+Shift+Z survived Save and Ctrl+S +restored the exact final oracle hash. Recovery folder then contained zero JSON +checkpoints and current stderr remained empty. This single-file byte oracle also +preserves the leading comment and all unselected source declarations. + +Native found that Fill alone did not prevent the vertical scrollbar overlaying +right-hand Reference pair actions. An18px content right gutter now visibly fixes +Use pair and Remove pair at1440×900 and1100×700; both sizes passed native inspection. +The same overlay affects Members identity actions and Order Down +buttons; semantic session worker owns the consistent gutter correction. Reverse, +scenario, branch-step and fragment variants retain automated fixture evidence but +their new native runs remain pending. No complete-release acceptance is claimed. + This is current-machine evidence on `codex/release1-continuation`, integrated baseline c091f80 plus the uncommitted continuation. It does not supersede unrelated historical Linux checks or claim first-release acceptance. @@ -166,3 +207,112 @@ Native resize to1100x700 retained all9navigation pages. The Engineering guide at that minimum size showed the entire Clear control and wrapped readable article text, closing the observed clipping finding for that size. Ordered review and reference-pair modal native acceptance remain pending. + +## Reverse ordered review and transient Windows journal failure + +On the same D402 reference-padding binary, at1100×700, opened semantic-ordered-reverse.arcadia and explicitly selected System changes in Operational. Added Request/Energize/Verify OA→SA reference pairs (plus existing owner pair). Chose both Verify step addresses and Take System. Native refused ambiguous insertion with the expected unique-anchor error. Set final Steps order, moved Verify once before Energize, retaining Request and Local self-test, then applied two decisions atomically with zero extra impacts. + +Save As to .local/native-qa/ordered-reverse-result.arcadia initially failed before publishing its journal. Accepted model remained dirty, output absent. A retry to the same filename succeeded. Full saved source is byte-exact against semantic-ordered-reverse.after-order.arcadia, SHA25698020E048AC01F285C6AD8C10CE22BEF05E193B5BA0F5558BE1FB26A7BF82620. One Ctrl+Z restored three steps; Save initially hit the same failure while preserving prior accepted disk bytes, then succeeded on retry. Undo source matches pristine byte-exact, SHA2562B7C3694623BBEA3E36DCC6F674D4ADCD91E23C8E1005DED21E5A69648A048E5 (copy ordered-reverse-undo.arcadia). Ctrl+Shift+Z survived Save; final Save restored exact accepted hash. Recovery JSON count zero. Actual reverse Apply, retention after failed Save, Undo and Redo pass; failure-free save reliability does not yet pass. + +Independent public-core reproduction got18successful/2failed fresh saves with Windows Access is denied (os error5) at journal-directory publication. Plain/canonical/forward-slash path probes rule out the initial slash hypothesis. Source fix retries only transient Windows5/32/33 errors with a390ms total bound and checks for competing journal ownership before each attempt. This fix and full footer-error tooltip are source-only, awaiting tests and new native build. Existing native binary has no retry. Native window now maximized2245×1232, Traceability, saved reverse result; process8908/window853042 unchanged. +# Source-6 combined review native acceptance — 2026-09-06 + +Frozen GUI `.local/native-qa/bin-source6/arcadia-studio.exe`, SHA256 `5D0734D9BD6A87CCCA8FFC8C007962AEAC573ACB82E2974211FA857E93C18A72`, built with server after strict all-target Clippy. Isolated appdata and system-only PATH; libunwind.dll alongside. Initial process2452; controlled crash/restart process10236, window456983644. This binary precedes later Child/Incident and source5-checkpoint upgrades. + +Opened semantic-batch.arcadia through native file dialog. Normal Minimum120 correctly rejects against Maximum100. Opening Emergency trace retains Normal draft; Emergency Maximum `-` stays invalid. Excluding Emergency keeps its raw input, and changing Normal Minimum to5 produces a valid included preview. Reopening excluded Emergency preserves `-`, inclusion=false, and enabled Apply for the valid included row. All retained-row selectors, inclusion, error, input and Apply controls fit at1440×900 and supported1100×700 content dimensions (captures1442×932 /1102×732). + +Checkpoint `c33eb911-7792-4f95-ad6f-f0676a733d21` captured both rows exactly; copied `.local/native-qa/batch-before-crash.json`. Forced exit then Home Recover restores included Minimum5 and excluded invalid Maximum`-`, with a valid combined preview. Found misleading recovered add-trace label “Deleted reference” for a live trace; source correction and its focused test subsequently passed in the92-test semantic run, but native recheck of that label requires the next build. Input150 needed refocus/retry once when the first tool text entry left the field blank; the confirmed second entry produced150. No application defect is inferred from that isolated input event. + +Included repaired Emergency Maximum150 and set Normal Minimum120, then applied once. Save as `.local/native-qa/source6-batch-result.arcadia` matched the independent final source exactly: SHA256 `D3315BBA762395205AE761E6E64029E8B4070C15A1299C6114075581DB7B0974`. One Undo then Save matched the initial source exactly: `5E1F433E04621424EDAC43E53411624059E873F88DD531BFDB39093953485ED7`; copied source6-batch-undo.arcadia. Redo after Save, followed by Save, restored the final hash exactly. All three saves succeeded first attempt; checkpoints retired; stderr remained empty. These source oracles include both trace observations and unchanged source records/comments, not merely a displayed numeric value. Dense views, CM direct authoring, latest attachments and source5 checkpoint native acceptance remain separate gates. + +## Dense saved view duplication and explicit hide — independently reopened + +On the same source-6 native binary/process10236, root observed the default1440×900 +Fit89% view with all40 labels readable and no overlap, and inspected actual +exchange01/10/20/40 identities. Commands category filtering displayed10; one Undo +restored All. At1100×700, Fit52% is a tiny overview, while99% working zoom plus +blank-canvas pan allowed selection of all four named exchanges/aliases without a +model edit. These observations do not establish a need for grouping. + +Root then saved the Commands-filtered original and its duplicate, named +`Commands review`, with explicit Hide on Service data05. Save succeeded first +attempt. The actual saved entry is `.local/native-qa/source6-dense-result.arcadia`, +SHA256 `B91F2C4D9A9B1257B426F2F36F1A7948491390E0CF36B592244B925A93686DFD`. +It includes `views/diagram_forty_exchange_view_copy.arcadia`, SHA256 +`898E4E7C6B835D9CF1E1E6810F4BC25AC5AF93CABBF547ACABC9827DB2561343`. + +Independent read-only verification reopened **both files** through production +`source_files::load_project` and `source_project::compile_bundle`, validated the +result, and compared it against both pristine source and pristine JSON. The +pristine source SHA256 is +`00B87736B82C5AFD33B311DF7AEBE9A6C8C51B6DE66221181B5EA8BC8CA221A3`; +pristine JSON SHA256 is +`5DD4F22453330CAB3ABC059881660190E689A22619853F77CF6D0B52735D97AD`. + +Exact whole-Project equality passed after constructing only these expected changes: + +- Original diagram `d4060906-0000-4000-8000-000000000fa0` changes only its Work + categories to Commands (`concern_1`), retaining every other persisted field. +- New diagram `d5841bc0-4c1a-494d-b79f-e3ad872d1f38`, alias + `forty_exchange_view_copy`, is named `Commands review`. It copies the filtered + original's fields and adds exactly Service data05 to its explicit hide list. +- Its two fresh placement IDs are `6ce88797-657c-4b11-88af-119dea388be4` and + `f85547ce-f80b-4b4d-be76-77c5c84f0449`, with the corresponding `_copy` aliases. + Element references, positions, sizes, collapse, ports and persisted routes match + their original placements/view exactly. All three new IDs are distinct and absent + from the pristine model's complete identity set. + +All46 elements,42 relations, traces, artifact links and other project facts remain +exact. Production projection yields exactly exchanges01–10 for the original and +01–04 plus06–10 for the copy: **10 original /9 copy**, with two nodes each. Automatic +route/label positions were not compared: they are recalculated and labels repacked +after hiding a line. No model or source file was written by this verification. + +Reproduction helper `.local/verify-dense-native.rs` was compiled directly against +the existing `liboa_core-6c8da57367eec642.rlib` (SHA256 +`21ED1DDBEDF881464E049C1A9A0D3C1A6479E3FD9372F85B23ECDEE6BC148842`) with +task-local Rust1.98.1/clang; no Cargo or server process was used. Its exact passing +output is `.local/dense-native-oracle.log`. After dot-sourcing +`.local/toolchain/env.ps1`, the invocation was: + +```powershell +rustc --edition=2024 .local/verify-dense-native.rs --extern oa_core=.local/target-gnullvm/debug/deps/liboa_core-6c8da57367eec642.rlib -L dependency=.local/target-gnullvm/debug/deps -C linker=clang.exe -o .local/verify-dense-native.exe +.local/verify-dense-native.exe .local/native-fixtures/dense-40/dense-40.arcadia .local/native-qa/source6-dense-result.arcadia .local/native-fixtures/dense-40/dense-40.json +``` + +**New native finding, still open:** switching from the saved copy to the original +through the diagram selector changes the canvas/selector to the original's ten +lines, but the inspector retains the copy's name, alias and property draft. Source +review confirms `Message::Diagram` changes `active_diagram` without changing the +previous diagram selection before rebuilding its draft. A guarded current-view +inspector follow correction is proposed; no fix or new-binary acceptance is claimed +by this record. Other selector/label/coverage combinations and fresh GUI reopening +remain separate native cases. + +Subsequent native reopen passed on `.local/native-qa/bin-source6-recovery`, desktop +SHA256 `DD5D01FA07B921F1A7B365C33867876A7AE1BCBD01815B4BB4206408D2012F95`, +process9716/window5702656: original10 includes05, copy9 excludes05, and the copy's +new count reads9 shown /33 omitted. This verifies saved-view reopening and the +count on that binary. It precedes the newly prepared diagram-inspector follow fix +and Artifact implementation. The inspector correction has six focused tests in +`app/diagram_selection_tests.rs`, and those tests passed in the later full 518-test desktop run. Strict all-target +workspace Clippy also passed (`.local/source6-artifacts-final-clippy3.log`). Rebuilt +native acceptance remains pending; automated results do not close this native finding. + + +## Latest automated artifact evidence — native gate remains open + +The later full desktop suite passed **518/518** in `.local/source6-artifact-inspector-desktop-tests2.log`. The artifact picker’s fresh-editor falsifier exposed an artifact-ID/owner-layer classification error; after correction its six-test filter passed in `.local/source6-artifacts-final-tests2.log`. Strict all-target workspace Clippy then passed in `.local/source6-artifacts-final-clippy3.log`. This automated evidence covers the implemented artifact lifecycle and the inspector correction; it does not establish native acceptance of either. + +Artifact move, source-attachment-absence deletion and counterpart-creation fixtures, direct-core requests, exact source/JSON oracles and the reproducible generator are preserved under [`fixtures/release1-native/artifacts`](fixtures/release1-native/artifacts/README.md). Artifact native acceptance remains pending. The source-6 batch and dense-view native observations above retain their exact earlier-binary scope. + +## Scenario ordered-member variant — 2026-09-06 20:10 PDT + +AB6ECD62 GUI process2540 at1100x700 opened the tracked schema5 semantic-ordered-scenario.arcadia. Import reported5→6 with0semanticdefaults/0interfaceallocations/0unresolvedvalueaddresses. Explicitly added Requeststart, Energize and Verifyinterlock OA→SA reference pairs. Members/rows Orderedsteps staged the exact Verifyinterlock occurrence pair; TakeOperational rejected with the expected no-unique-unchanged-anchor message and disabled Apply. + +Reviewfinalorder→Setfinalvalue preserved the local self-test and appended Verifyinterlock. One Up placed it between Localself-test and Energize. Validated preview showed0additionalchanges and enabled Apply. One Apply followed by SaveAs .local/native-qa/source6-ordered-scenario-result.arcadia matched the complete original direct-core after oracle with only the format-header token changed5→6. One Undo/Save matched the similarly header-upgraded initial oracle; one Redo/Save restored the complete after oracle. No normalization of identifiers, fields, whitespace or history was performed. All3saves firstattempt; recoveryJSON0; no new stderr. + +- Applied/Redo SHA256 C0781CD5E6B990405A6DE3A5325EE438F0BC21376AE7ABDF519CAD7B0D2BDE25. +- Initial/Undo SHA256 F6AF0341433FBE22BDECF32B15567B8568D3F338A38CC00E0A0F008C815EA0E1. + +Both picker columns, explicit reference pairs, order rows/UpDown, error and footer actions were usable at1100x700. This extends exact native ordered review to the Scenario owner variant; Branchsteps/Fragmentbranches remain separate pending variants. No new crash/recovery cycle was executed for this variant. Clean AltF4 followed final saved state. diff --git a/harness/release-engineering-guide-audit-20260906.md b/harness/release-engineering-guide-audit-20260906.md new file mode 100644 index 00000000..fb49bb9d --- /dev/null +++ b/harness/release-engineering-guide-audit-20260906.md @@ -0,0 +1,81 @@ +# Built-in engineering guide: current workflow audit + +Audited the 16 existing offline topics against the user objective, current controls, +normative language contract and committed checkpoint `2b2d2bc`. Expanded the guide +to **18 navigable/searchable topics** by separating semantic review from counterpart +creation and engineering checks from crash recovery. Only `guide.rs` was edited; +the recent Fill constraints on search, article content, step rows and Clear control +remain intact. No new dialogs, model mutations or application navigation paths +were introduced. + +## Existing-topic assessment and changes + +| Existing topic | Assessment and bounded correction | +|---|---| +| First model | Creation tutorial was useful but stopped before a reviewable OA→SA baseline. Added mission/capability→operational behavior→reviewed system contribution→contracts/states/evidence→checks/source milestone. | +| OA and SA | Retained explicit need versus system contribution and later-layer boundary. No claims of full Capella/SysML conformance. | +| Participants, roles and work | Retained direct performer versus role responsibility and work decomposition. | +| Ownership/allocation/views | Existing independent placements, collapse, routing, manual ports, SVG and hide/delete distinction remain accurate. Selective filters/categories remain a later integration section below. | +| Missions/capabilities | Replaced generic scenario-context prose with the actual Scenario Context property and explicit Chain involvement workflow. | +| Exchanges | Scoped typed component-port/Boundary drawing to SA. Explained current Legacy OA compatibility and that ordinary participant Communication means are not yet offered. | +| Processes/chains | Replaced stale +Work/+Exchange/Connect instructions with actual Work, Exchange, Sequence link, Select / reorder, Two paths… and imported ordered-path conversion controls. | +| Scenarios | Added Draw message source-position→receiver gesture and selected ↕ reorder handle; retained precise forms, fragments, reuse and correlated call/reply. Clarified occurrence-specific values versus shared exchange definitions. | +| States/modes | Added selectable typed Entry/Do/Exit rows without fabricated containment and a timed transition editing recipe preserving existing triggers/guard/effects. | +| Data/interfaces | Existing bounded primitives/enums/records, multiplicity/defaults, inheritance/reference recursion, parameter phase, identified interface allocations and port contracts retained. | +| OA↔SA counterparts | Removed stale “ordered collections unfinished” statement. Kept creation/scope/reuse distinct from typed drift decisions and linked to the new review topic. | +| Artifacts | Named the actual typed References/Satisfies/Verifies/Implements choices, with URI/revision/fragment, owner and non-fetching behavior retained. | +| Text/Git | Added explicit include syntax and the risk of reviewing only an entry file when an edit adds a declaration in another file. | +| Checks/recovery | Split engineering completeness into its own topic. Existing raw invalid property/source/behavior/workflow retention, private checkpoint delay, detached Save As and absent recovered Undo stack remain explicit. | +| Files/shortcuts | Added actual Home Continue editing label and source-entry Save As history/Redo preservation, include-name collision and complete-file save guidance. | +| Hosted/REST | Added source-project candidate validation versus conditional commit, warning-only validity and source-location parity; preserved 412/unknown-outcome and no automatic merge semantics. | + +The new **Review semantic changes** topic covers trace/direction selection, +Previously reviewed versus Current, Keep/Take/final candidate decisions, exact +reference pairs, qualified annotation paths, lifeline bindings, row presence versus +identity, membership versus order, Reconcile with selected membership, exact history +resets and one-transaction apply/Undo. It explicitly identifies the current one-trace +dialog; it does not present independent sequential reviews as a multi-source batch. + +The new **Read model checks** topic explains structural rejection versus advisory +warnings, structural gating of completeness, responsible leaf work and typed ports, +actual included occurrence/reuse evidence, mission-capability links, explicit +capability behavior context, group exclusions and the limits of coverage. Its search +keywords include the five actual diagnostic codes so pasted codes lead to guidance. + +## Evidence and verification boundary + +Controls and meanings were checked in `behavior/topology/view.rs`, +`behavior/scenario.rs`, `behavior/states.rs`, `behavior/state_canvas.rs`, +`semantic_review/view.rs`, `semantic_review/ordered.rs`, semantic recovery, +`home.rs`, `app/checkpoints.rs`, document Save As, forms, core coverage and server +routes. The actual typed timed-save result and complete included-file preservation +are established separately by `release-timed-bundle-oracle-20260906.md`; REST +candidate/rejection/restart parity by `release-rest-coverage-verification-20260906.md`. + +Ordered review was source/core tested when this guide was written; its current +native acceptance was still in progress. This audit does not elevate that evidence +to native verification. Prior minimum-window guide acceptance demonstrated the +existing Fill/Clear layout fix, not these expanded articles. + +Owned-file rustfmt and diff-check pass. No Cargo or native input was performed by +this guide worker. The existing guide search/navigation tests should run with the +next coordinated desktop check. Native acceptance should search “semantic order”, +“work_without_behavior_context” and “time event”, follow related topics and workspace +links, and read long walkthroughs at the normal and minimum window sizes. The +result must retain the current model and raw drafts and expose all text without +Clear-button or scrollbar clipping. These checks remain for the native operator. + +## Later integration hooks — not current feature claims + +| Slice that must land first | Guide follow-up and acceptance needed | +|---|---| +| First-class OA CommunicationMean | Replace the temporary boundary limitation with the actual participant→participant gesture/form, orientation and actual-performer Interaction allocation rules. Explain legacy records without automatic conversion; include call/reply semantics only after that contract is frozen. Verify the named controls and save/reopen example. | +| Persisted selective views/categories | Expand the views topic with actual Work/Participant category selectors, All versus empty selections, hide/kind/category/context intersections, omitted-edge reasons and labels. Distinguish global behavior coverage from selected Process/Chain/Scenario scope. Explain summary member selection only if the dense-diagram acceptance gate requires and verifies it. | +| Attached records/artifact reconciliation | Extend semantic review with exact attach/remove, owner/endpoint choices, artifact-field decisions and collateral changes once authorable; do not imply that a current mapped reference creates or reparents records. | +| Multi-source review | Replace the explicit one-trace limitation only after a shared final-candidate workflow exists. Teach source selection, competing proposals to the same destination, shared reference mapping, exact per-trace observation decisions and one combined preview/commit. Sequential Take operations are not an adequate substitute. | +| Source/schema6 freeze | Update examples/version language only with real migration/admission tests and source/GUI/API round trips. Keep the current source5 guide accurate until then. | + +The current `docs/scope.md` still contains stale unfinished statements about ordered +collections and the now-implemented completeness diagnostics. The root was notified; +that file is outside this worker's ownership. This is a remaining documentation +synchronization item, not a reason to repeat those stale claims in built-in help. diff --git a/harness/release-final-native-priorities-20260906.md b/harness/release-final-native-priorities-20260906.md new file mode 100644 index 00000000..76bd7061 --- /dev/null +++ b/harness/release-final-native-priorities-20260906.md @@ -0,0 +1,64 @@ +# Final native priorities — 2026-09-06 + +Read-only triage against the objective, current STATE/TASK_LEDGER, coverage audit, +and later native reports. No Cargo or native operation. These are **eight bounded +actions**, not a Cartesian product of layer, platform, size and every enum. Root +should use the newly accepted binary/assets and retain its exact hash. The Linux +worker's current application gate proceeds independently. + +## Prioritized acceptance matrix + +| Priority / classification | Existing fixture and smallest useful task | Required observation / stop condition | +|---|---|---| +| **1 — known material defect, source fix awaiting native acceptance** | `fixtures/release1-native/communication-means/cm-reverse-scenario.arcadia`; repeat only the reverse scenario transition on the new binary, using its existing request/picker instructions and distinct `check reverse-behavior` mode. | Apply selects the explicit OA scenario counterpart in Behavior, preserves Call/Reply correlation and all saved diagrams, then exact Save/Undo/Redo. New oracle expects ten generated target/trace UUIDs and **zero placements**. Preserve legacy reverse's passed fifteen-UUID/five-placement result. No repeat of drawn CM/create-one/reuse-both trials. | +| **2 — new implementation and corrected teaching assets, untested together** | Copy complete `examples/release1/`; exercise Home/guide Open walkthrough at1100×700, then open the working copy. Follow README sections1,4–6: responsibility versus work, mission/include context, interface/data contract, deliberate warning, and reviewed `oa_confirm`→`sa_confirm` name edit. | Dirty Cancel retains raw input; opening/replacing uses the normal guard. All30 source modules and evidence target remain available; original packaged folder unchanged. Exactly the located warning in `expected-diagnostics.json`; inspect/refine it through its located object. Reviewed rename updates only intended source/history; Save/reopen preserves it. Guide actions remain reachable. This is the practical onboarding/parity story, not another full CM replay. | +| **3 — implemented behavior editing; known compact-form correction lacks native closure** | Working copy of `examples/release1/project.arcadia`, Process `oa_response` and referenced `oa_field_procedure`, plus Scenario `oa_full_scenario`/`oa_conversation`. At1100×700 open a Two paths/contribution form, edit both path values/guards, Cancel then commit one bounded change. Inspect and edit one explicit reuse binding. Use `fixtures/release1-native/semantic-ordered-fragment.arcadia` only for the still-uncovered Fragment branches final-order control. | Form fields scroll independently; Apply/Cancel stay visible; canvas/owner/camera remain stable. Real pointer gesture produces correct same-context links and no duplicated shared work; rejected reuse/order retains drafts. One exact model/source/history comparison per distinct committed change. Fragment order uses its supplied ambiguous/final request and `.after-order` oracle; source5→6 header migration must be explicit. No need to repeat main/reverse/Scenario ordered checks or every ALT/PAR/LOOP combination. | +| **4 — known inspector correction plus untested saved-category lifecycle** | `fixtures/release1-native/dense-40/dense-40.arcadia` or the retained `.local/native-qa/source6-dense-result.arcadia` original10/copy9 result. Switch saved original/copy while inspecting the view; make a category membership edit and delete that category on a disposable copy. | Inspector follows the selected view; an unfinished draft obeys Apply/rejection guards. Category deletion keeps semantic exchanges and removes/repairs references through the documented command. Undo restores category/filter membership, placement/route facts stay exact, Save/reopen preserves the result. Use `dense-40-oracle.json` for existing visibility cases; derive a specific command oracle before the new lifecycle edit. Do not repeat forty-label inspection or all eleven filter matrices. | +| **5 — bounded state presentation gap, not absent typed actions** | `fixtures/release1-native/state-actions.arcadia`: fold/unfold the nested region, rename one linked work target, inspect its typed action row, Save/reopen the saved state view. | Typed action retains its real relation identity and refreshed name; Internal transition/nested region coexist; saved view routes back to Behavior. Exact source/history for the rename; folding preserves semantics. All three Entry/Do/Exit selections, compact Exit selection, timed-event rejection/recovery and direct state-create/Undo already have native evidence—do not repeat those whole trials. | +| **6 — corrected artifact wording only** | `fixtures/release1-native/artifacts/semantic-artifacts-create.arcadia`: reserve/stage the explicit destination; Take Presence plus Keep Owner to expose the reviewed conflict, then repair or Cancel. If needed, use `semantic-artifacts-move.arcadia` for kept Presence versus moved Owner. | Reserved alias/name is readable in picker, facet title and conflict; no false Deleted reference label. Same-row facets are identified accurately, Apply disabled for conflict and no model mutation. This short display regression closes the new wording; CREATE/MOVE/DELETE exact history and raw crash recovery already passed and need no replay. | +| **7 — fixed hosted destination isolation and current source6 integration** | Publish a disposable complete `examples/release1` SourceBundle to a fresh loopback store. From its hosted session open a copied entry with a deliberate syntax error, repair it and Save locally; separately cancel/fail a local Open. | Repair-workspace Save never changes the old server resource/ETag; Cancel/read failure retains the hosted destination. Catalog clearly reports stale content after a write and explicit Refresh updates it. The lesson's source6 categories/CM/artifact fields survive server import/read/restart. Existing real stale-ETag conflict, offline retry and local-copy native trials already passed; repeat them only if this targeted run exposes a new transport regression. No old D:/ fixture is required. | +| **8 — platform acceptance, not a Windows rerun of every feature** | On the current Linux application build, use the same complete lesson working copy for one compact direct behavior edit and one typed data/interface edit, then Save/reopen and normal close. Use its `model.json`/source bundle for the Linux server import/validation/restart gate already assigned to the worker. | Working renderer/input at supported size, readable pending forms, exact persisted model/source and no serious runtime output. Cover one still-unverified data association/port-allocation operation if its contract is selected before the trial; no invented expected output. Prior ext4 core/syscall passes and older Wayland native trials do not establish current GUI/server/package acceptance. A headless guest server pass alone cannot close desktop support. | + +## What this matrix deliberately does not reopen + +Later reports supersede the old coverage audit's missing-CM, missing-selective-view, +missing-completeness and missing-semantic-facet statements. Those implementations +exist. Native batch, Child/Incident, source5 recovery, artifact lifecycle, keyed +Take/repaired Binding, CM drawing/port alternatives, ordered main/reverse/Scenario, +guide navigation, dense duplicate/hide/reopen, and state timed-draft recovery are +scoped passes. A new executable hash requires targeted changed-path checks and +final automated regression, not automatic repetition of every historical task. + +Optional permutations after these gates include every pseudostate/trigger, both +directions of every scalar/attachment facet, each fragment operator at both sizes, +every filter intersection, all role/capability graph permutations, and a new crash +cycle for every already-tested raw adapter. Escalate only when a distinct uncovered +control or observed failure gives such a case a concrete purpose. Branch-steps +order shares the exercised ordered-row engine; Fragment branches is the higher +value remaining native collection shape. Automated contract coverage remains +necessary for both. + +## Scope reconciliation and non-native release gates + +- `docs/scope.md` and the fixture root README still call artifact/attachment/CM + native work pending, despite later passes. The historical audit and append-only + native report also retain superseded gap text. Update current-facing summaries + by pointing to the later exact reports; do not erase historical evidence. +- F2 is a **root scope decision**, not a user-approved reduction or a passed + behavior-export feature. `docs/scope.md` and the guide still use broad “SVG + export”/“diagram image” wording; explicitly qualify architecture-only export. + The objective asks for fully feature-complete OA/SA according to the application + specification, while scope.md describes a bounded Studio profile. Final domain + review must resolve whether each excluded semantic capability is outside that + specification; passing this matrix cannot settle that interpretation alone. +- Final current-tree full-workspace/MSRV/docs/OpenAPI/source/example checks, + optimized Windows/Linux artifacts, notices/runtime/complete lesson packaging, + clean-environment launches and fresh independent end-state review remain release + gates. These should be scheduled once on the accepted tree, not added as another + set of native semantic permutations. Existing QA packages and old aggregate test + totals are not final distribution evidence. + +Primary evidence: [STATE](STATE.md), [coverage baseline](release-current-coverage-20260906.md), +[current native](release-current-native-verification.md), [CM](release-communication-native-20260906.md), +[artifacts](release-artifacts-native-20260906.md), [hosted](release-hosted-desktop.md), +[process](release-process-editing.md), [independent navigation review](release-behavior-transition-independent-review-20260906.md). diff --git a/harness/release-guide-native-20260906.md b/harness/release-guide-native-20260906.md new file mode 100644 index 00000000..93205f0f --- /dev/null +++ b/harness/release-guide-native-20260906.md @@ -0,0 +1,13 @@ +# Engineering guide native review — 2026-09-06 + +Actual Windows AB6ECD62 GUI, isolated process2540. Built-in guide displayed18topics at1440x900 and1100x700. Search, topic list and independently scrollable article remained readable; steps wrapped within the article panel, and all nine main page actions were visible at minimum width. This is scoped navigation/layout evidence, not a completed review of every article or every described workflow. + +Searched scenario:9matchingtopics. Selected Scenarios & alternatives, which cleared the query and opened its heading. Scrolled the full article through message/lifeline creation, Call+correlatedReply, canvas Drawmessage/reorder gestures, ALT/PAR/LOOP/OPT, explicit destination operands, scenario reuse, execution-boundary explanation and related links. Text/step numbers stayed within the article panel at1100x700. + +Found a navigation defect: from the bottom of Scenarios, clicked related Exchanges & dataflow. Sidebar selection updated but new article retained prior scroll offset and opened at its category tail/related links, skipping its heading and introduction. Assigned focused topic-scroll correction to behavior_review after the frozen Windows diagnostic build chain. Corrected binary/native retest is pending. + +No-match search zzznomatch produced0topics with a clear explanation and Clearsearch action. Clearsearch restored18topics and the Exchange article at its heading. This empty/recovery state passed. FirstProject step06 still says Saveproject while current top bar says Saveall; minor wording correction is recorded separately. Guide actions did not dirty the saved model. + +## Corrected binary native recheck + +Windows process9800, GUI SHA-256 A5A741349B49D0C90A6BDB0FA7871415133CBCC1B8A4ECF0C4EDDDB99313F2D2, isolated bin-source6-guide. At1100x700, moved from the bottom of Scenarios & alternatives through its related Exchanges & dataflow link: the new article opened at its heading/introduction. Scrolled Exchanges to the bottom, then searched exchange:10matches, current article and exact bottom position retained. Replaced query with Git:4matches, automatically selected Build your first model and opened at its heading. These observed transitions pass the reported regression and both search cases. No model dirty marker appeared. FirstProject step06 Saveall wording was also observed at1440x900 before this minimum-size recheck. Three focused navigation tests, strict workspace/all-target Clippy and GUI/server build passed in .local/guide-navigation-{tests,clippy,build}.log. This is scoped guide acceptance; it does not claim every article's described engineering workflow was executed. diff --git a/harness/release-interface-operation-contract-20260906.md b/harness/release-interface-operation-contract-20260906.md new file mode 100644 index 00000000..3760fd7e --- /dev/null +++ b/harness/release-interface-operation-contract-20260906.md @@ -0,0 +1,147 @@ +# Interface-operation message contract — proposal and falsifier, 2026-09-06 + +Status: contract/experiment only. No shared production edit, Cargo, or native input. +This proposal requires independent critique and root approval before implementation. + +## Experiment outcome + +[Standalone existing-core probe](fixtures/interface-operation-probe/README.md) +builds a valid System/Actor boundary with one provided/required interface and two +Synchronous Operation items, Reset and ReadStatus. Both have zero parameters. Calls +with the same participant pair/channel have identical typed bodies despite their +different names. Item/allocation substitutions for `exchange` reject; an operation +JSON field rejects. Reset rename/delete does not affect its intended Call body, +which remains structurally valid. This is a missing maintained semantic reference. + +A valid alternative adds two Functions, four function ports, two FunctionalExchanges, +two performer allocations, four port allocations and two exchange allocations. +Its Calls select distinct FunctionalExchanges while retaining the original boundary. +That is **six additional elements/ten relationships**, not a different notation for +the same interface-only model. It is appropriate when the functional refinement is +part of the engineer's design; it must not be fabricated solely to address an +interface operation. All three probe models pass complete JSON/source round trips. + +## Alternatives and selected direction + +| Choice | Tradeoff | +|---|---| +| Name/guard or generic constraint | Can describe intent, but supplies no closed operation-use slot or automatic endpoint/lifecycle validation. Does not close the demonstrated gap. | +| Select only ExchangeItem | Distinguishes Reset from ReadStatus but loses which interface allocation supplies its protocol/acquisition context; one item may occur in several interfaces. | +| Select InterfaceItemAllocation | Reuses an existing identified relation, preserving interface, operation item and protocol context. Recommended. | +| New operation occurrence constructor / whole message-target union | Can support channel-free interface scenarios later, but broadens constructors, old histories and UI beyond this demonstrated shared-channel case. Not required for the first slice. | +| Infer sole carried Operation item | Ambiguous with multiple or zero-parameter operations and unstable when another item is added. Reject inference even when there happens to be one item. | + +Proposed persistent field in existing `OccurrenceBody::Exchange`: + +```rust +#[serde(default)] +operation: Option // InterfaceItemAllocation identity, never an item or port +``` + +Text: `operation @reset_service_allocation;` or `operation none;`. JSON uses the +existing externally tagged Exchange record with this optional UUID. No separate +stored item/interface/provider fields: derive them through the selected allocation +and validated message endpoints. The field is authored on Call/Asynchronous; +Reply derives the operation from its exact correlated Call and must store None. +This prevents two redundant identities disagreeing after edits. Display the +derived operation on Reply as read-only provenance. + +## Applicability and validation + +This bounded slice applies to SA `BoundaryInterface` scenarios with an explicit +BoundaryExchange channel, participant lifelines and concrete component endpoints. +Existing operation=None scenarios, including Functional and MixedLegacy, retain +their prior meaning. New operation selection in MixedLegacy requires explicitly +choosing BoundaryInterface; it must not silently reinterpret that viewpoint. +OA participant channels remain operational communication and do not acquire +invented technical ports/interfaces. Channel-free interface messages are a +separate future design, not implied by this field. + +Resolve the actual sender/receiver participant and corresponding boundary ports +using the existing directional scenario resolver, including reversed traversal of +an unoriented channel. Never assume stored boundary source is the caller. +Require: + +- Existing same-SA-layer InterfaceItemAllocation whose target is an Information + ExchangeItem with mechanism Operation; the channel carries that item through + the existing effective-item/subtype rules. +- Concrete Standard component ports. The actual receiver provides an interface + exposing that exact allocation; the actual sender requires a compatible + interface also exposing it. Reuse `effective_interface_allocations` and existing + `interface_satisfies` inheritance rules. An item present through a different + allocation is not interchangeable with the selected protocol context. +- Call selects Synchronous transmission. Asynchronous selects Asynchronous + transmission and has no correlated Reply. Unspecified transmission remains a + visible incomplete-contract warning; explicit incompatible transmission rejects. + Do not invent transport multicast/broadcast call-return semantics in this slice. + Existing acquisition compatibility remains governed by the allocation validator. + +Known wrong kind/layer/port/interface/direction/mechanism is a structural error. +Unfinished sender/receiver or missing operation after an explicit operation-use +edit is a warning while the candidate is otherwise safe; no automatic endpoint or +operation choice. The exact warning distinction needs an operation-intent marker +if None must distinguish ordinary channel calls from cleared operation calls: +**prefer a closed `OperationSelection::{Unselected, Selected(Id)}` optional field +if testing confirms the plain Option cannot retain that distinction.** See the +open decision below; do not silently encode intent in names/history. + +### Calls, replies and values + +Existing exact call/reply identity, order, reverse participant and repetition +checks remain authoritative. Reply derives the operation from its Call; an +independent Reply selection rejects. Editing Call's operation replans/validates +all correlated Reply values atomically before admission. + +Use the selected item's effective data-field paths, not the union of every item +on the channel. Call values permit In/InOut; Reply permits Out/InOut/Return. +Unspecified parameter direction warns until clarified. Asynchronous operation +values use input parameters and cannot carry Return values. Parameterless calls +need no fabricated field. Keep raw value drafts on operation changes; reject or +require explicit value removal when a retained field is no longer applicable. +Never insert defaults or prune authored expressions merely on picker change. + +## Lifecycle, transition and representation obligations + +| Surface | Required contract and falsifier | +|---|---| +| References and deletion | Visit allocation selection as a typed relation reference. Rename shows the new item/interface label without rewriting message prose. Reidentify allocation/item/interface preserves selection through existing visitors. Deleting allocation/item clears selection to an explicitly unresolved operation use, retains Call and correlated Reply, prunes only proven invalid live field references under normal delete policy, and creates a located warning. One Undo restores all identities/source. Removing provision/channel changes must validate dependents or offer an explicit repair batch; never switch to another matching allocation. | +| Transition | Preserve source. OA→SA existing operation=None remains None; choosing a technical operation is an explicit SA refinement. SA→OA has no direct operational operation contract in this bounded design: preview must require explicit reviewed omission/abstraction, not silently drop the selection or invent OA ports. Proposed per-occurrence request decision `OmitOperation { source, rationale }` is needed if this transition is allowed; unknown/irrelevant decisions reject and omission is shown as a planned impact. Without that decision, refuse with a precise unsupported-projection message. Keep mapped channel, Call/Reply and values only when independently admissible. Same-layer/review mapping resolves allocation identity through its interface/item counterpart. | +| Semantic review | Add a closed optional allocation-reference facet, e.g. `OccurrenceFacet::Operation`, to observed/current/history and constructor scope. Compare its actual field slot; preserve invalid candidates and excluded rows. Derived Reply operation has no second independently writable slot. Operation changes plus value edits must compose on one frozen base; conflicting selections remain explicit. Operation omission across layers is not an ordinary mapped Take. | +| Source/JSON/API | Current source6 rejects new fields; introduce the next schema/source version coherently with any approved S1 change. Old input defaults to ordinary exchange behavior, never guesses an operation. Extend parser/writer/updater/reference spans, strict schemas/OpenAPI examples, migration rejection tests and commands; old historical DTOs remain frozen. Every modified relationship/value and history update must preserve unrelated source/comments and one command boundary. | +| Desktop | In the existing scenario form, show an optional Operation-use control followed by a named allocation picker (`Interface · operation · protocol`). Filter choices by the proposed actual participant/channel context; no name inference. Preserve a rejected selection visibly when context changes rather than silently dropping it. Call and Reply labels make the selected/derived operation readable, including parameterless messages. Use the existing scenario canvas and draft/recovery machinery. | +| Recovery and history | Persist exact operation intent/selection and rejected raw drafts; rebuild previews against the recorded base. A source-version migration must upgrade checkpoint accepted bundles/fingerprints through a proved adapter and reset approvals, not merely change schema numbers. Both old-v6 normal restore and new-version exact restore need tests; prior source5 recovery does not automatically prove this. | + +## One open DTO choice to settle before coding + +Plain `Option` is the smallest structural addition but conflates an ordinary +channel Call with an authored operation Call whose allocation was deleted. To +preserve explicit unresolved intent without inventing a tombstone, recommend the +following **final candidate**, replacing the shorthand above: + +```rust +operation: Option +struct OperationUse { allocation: Option } // deny_unknown_fields +``` + +None means ordinary existing channel-message semantics; Some(allocation=None) +means operation use requiring selection; Some(Some(id)) is a selected allocation. +Reply still stores None and derives the Call's OperationUse. This is one authored +semantic slot, with a maintained live reference inside it. Text renders +`operation { allocation @reset_service_allocation; };` and unresolved +`operation { allocation none; };`. Migration default is None. Review must preserve +the outer None versus explicit unresolved shape, as it already does for other +closed optional/constructor observations. This is the recommendation to critique, +not a field already added to core. + +## Decisive next tests + +Before GUI wiring: same-boundary Reset/ReadStatus selections differ and survive +rename/reidentify; selected allocation deletion yields unresolved operation intent; +two interfaces containing the same item retain distinct protocol identities; +inherited provider/required interfaces resolve the actual allocation; reverse +unoriented channel uses actual caller/receiver; wrong provider/mechanism/protocol +rejects unchanged; changing Call operation invalidates incompatible Reply values +without data loss; explicit SA→OA omission versus missing decision; both semantic +review directions with mapped/NotApplicable slots; full source/API/history/recovery +round trips. Include the original parameterless fixture so parameter-value +inference cannot accidentally substitute for operation identity. diff --git a/harness/release-interface-operation-contract-independent-review-20260906.md b/harness/release-interface-operation-contract-independent-review-20260906.md new file mode 100644 index 00000000..bcdf43f5 --- /dev/null +++ b/harness/release-interface-operation-contract-independent-review-20260906.md @@ -0,0 +1,39 @@ +# S2 interface-operation contract — independent critique + +Scope: the proposed `release-interface-operation-contract-20260906.md`, its existing-core fixture/probe source and output claims, current interface/port/message-value semantics and primary Capella metamodel. No S2 production edit, Cargo, native action or independent rerun of its probe. The separate App navigation correction is outside this review. + +## Assessment + +The missing maintained operation identity is real for the demonstrated interface-only model: two parameterless Calls over the same boundary cannot be distinguished by their current typed message fields. Functional refinement can distinguish them, but adds engineering functions/ports/relationships that the original interface-only model does not assert. The proposed bounded addition is appropriate; it does not require channel-free messages or a whole new scenario constructor. + +Prefer the final `Option }>` shape. None retains ordinary channel-message semantics. Present/unresolved retains explicit operation intent after deletion or incomplete authoring. Present/selected names the maintained InterfaceItemAllocation, retaining its interface/item/protocol context. A plain optional item ID or plain optional allocation ID loses one of those facts. Do not infer a unique operation from channel items or message values, including the parameterless case. + +Primary [Interaction.ecore](https://raw.githubusercontent.com/eclipse-capella/capella/master/m2/plugins/org.polarsys.capella.core.data.def/model/Interaction.ecore), SequenceMessage lines106–120, describes an invoked AbstractEventOperation derived through the message ends. This supports a maintained operation reference distinct from message prose; it does not require reproducing those metamodel classes in Studio. The specific allocation-based selection is a Studio design choice justified by its already identified InterfaceItemAllocation and protocol semantics, not a claim of exact Capella wire compatibility. + +## Corrections required before freezing + +| Priority | Concrete risk/counterexample | Required decision/test | +| --- | --- | --- | +| P1 compatibility | Adding `operation: None` to the existing Exchange constructor can serialize `"operation":null` into every old Call/Reply. Current historical fingerprint adapters borrow current element bodies; changing only schema_version would then compute a different source5/6 workflow binding although source migration changed only the version token. | Preserve historical omission exactly (e.g. current serialization omits None plus frozen legacy adapters) and pin literal nonempty source5/source6 Call and Reply model/snapshot examples. Test exact pre-migration fingerprints and raw draft retention, not only empty projects. Explicit source1–6 `operation` fields, including null, must remain rejected. Coordinate this with S1's schema7 migration; no independent bump that leaves old checkpoint restore broken. | +| P1 reply context | A Call goes client→provider; its Reply goes provider→client. Applying the proposed receiver-provides rule to the Reply's receiver would wrongly demand the client provide the operation. Deriving the ID alone is insufficient if validation/picker helpers still resolve roles from the Reply as a fresh invocation. | Resolve allocation, provider, required interface and parameter signature from the **exact correlated Call**. Run existing Reply order/correlation/reverse-pair rules separately. Test a client that only Requires, provider that only Provides, void operation, reverse Call over an unoriented channel, and more than one possible provider port. | +| P2 interface scope | The proposal says “actual receiver provides” without freezing participant-level versus endpoint-port declarations. Studio permits participant interface roles, but `ports.rs:118` explicitly does not inherit them onto ports. A new resolver scanning participant roles could silently assign a participant-wide contract to an unrelated socket. | For this concrete-boundary slice, inspect the actual resolved component ports' explicit roles. Interface inheritance is via `data::interface_satisfies`; participant containment is not interface inheritance. A participant-only declaration is not proof of a port declaration. Either classify that incomplete case explicitly or reject operation selection until the port is declared; do not guess. | +| P2 unresolved signature | Deleting the selected allocation changes Selected→Unresolved while some Call/Reply field references may still exist elsewhere on the channel. Validating against the whole channel union would silently rebind those values; requiring a complete selected signature could instead make the promised deletion-to-unresolved operation impossible. | Preserve all still-live authored value expressions, mark operation-signature checking unresolved, and perform only independent structural/path checks until reselection. Delete only references proven invalid by normal deletion. A later selection must validate the retained values against its exact effective signature, with no implicit pruning/default insertion. Test deletion of allocation separately from deletion of item/fields. | +| P2 return-channel policy | Existing Reply semantics may permit another admissible channel. The proposal does not state whether an operation-derived Reply must use the Call channel or how a separate return channel proves payload compatibility. Parameterless replies will not expose a missing check through field validation. | Do not accidentally narrow existing operation=None replies. For selected operations, freeze whether a separate channel is permitted; if permitted, validate its actual endpoints and carried output contract separately from Call-derived provider/signature identity. Include a void return and a channel carrying unrelated data. | + +The recovery proposal currently says reset approvals. That is a safe conservative policy, but must be coordinated with the exact existing source5 adapter behavior and raw intent retention: clear execution-impact approvals if required by the new migration contract, never discard authored Keep/Take/Candidate inputs or accepted source facts. Never accept a saved command/plan merely because an old fingerprint was rebound. + +## Allocation identity and inheritance + +No defect found in using allocation identity rather than a bare ExchangeItem. `data.rs:75` returns the exact inherited allocation relations base-first; `data.rs:138` defines provider conformance by explicit ancestry. Requiring the selected allocation to be exposed through both the provided interface and an actually required compatible interface avoids silently substituting a same-named or differently allocated protocol contract. A provider's derived-only operation should not become callable through a required base interface that does not expose it. Test the inherited base allocation as accepted, the provider-only local allocation as unavailable through that base requirement, and unrelated interfaces allocating the same item as distinct. + +Selecting an allocation whose item has a specialized carried subtype should use the selected operation's effective parameter signature; do not admit arbitrary additional channel fields or silently select the subtype's other allocation. Existing item conformance can prove channel carriage while allocation identity fixes protocol/signature context. + +## SA-only slice and transition review + +Restricting new operation selection to explicitly chosen SA BoundaryInterface scenarios is a coherent first slice if unsupported selections fail visibly; operation=None Functional/MixedLegacy/OA records must retain their old meaning. The source and API must enforce the same rule as the picker. + +Reviewed SA→OA omission is justified because this slice has no equivalent operational allocation/port contract. It must be an explicit per-source occurrence decision with a displayed rationale/impact; missing, irrelevant, duplicate or wrong-direction decisions reject. The mapped scenario/channel/Call/Reply and values still need independent admission. Reply has no duplicate writable operation slot; its omitted operation follows the Call's exact abstraction decision. Ordinary semantic Take must not map a technical allocation into an OA slot that is NotApplicable. Source remains unchanged, and one accepted transition must retain exact source/history behavior. + +## Bounded acceptance recommendation + +Approve the outer optional OperationUse and allocation identity in principle, contingent on the five clarified boundaries above. Before GUI integration, test the original two-parameterless-operations case, inherited versus provider-local allocation, exact provider/required ports, correlated Reply context, allocation versus item deletion, retained invalid values, explicit reverse omission, and nonempty old5/6 checkpoint fingerprints. Then test readable Call/Reply labels and raw forms on the actual minimum-size canvas, followed by shared source/JSON/REST and Save/Undo/Redo/reopen evidence. A new UUID field alone does not close the workflow. diff --git a/harness/release-journal-publication-review-20260906.md b/harness/release-journal-publication-review-20260906.md new file mode 100644 index 00000000..d98e850c --- /dev/null +++ b/harness/release-journal-publication-review-20260906.md @@ -0,0 +1,33 @@ +# Journal publication and recovery review — 2026-09-06 + +Independent read-only re-review of the current `crates/oa-core/src/source_files.rs`. Both previously reported findings are addressed in the reviewed Windows implementation. No additional material defect was found within this bounded change. This review did not run Cargo, operate the native application, or modify production code. The integrated workspace run was still in progress; its result is not claimed here. + +## Findings before and after + +| Finding and reproducible failure | Revised behavior | Evidence | +| --- | --- | --- | +| A destination check followed by `fs::rename(stage, journal)` was not a no-replacement operation. Another process could create an empty destination directory after the check; modern Windows Rust could replace it. A nonempty, complete existing journal was a different case and was not shown to be replaced. | `rename_journal_without_replacement` uses `MoveFileExW` with flags zero. The OS operation itself refuses replacement, independently of the advisory precheck. The stage remains owned by its temporary-directory handle until publication succeeds. | `.local/save-empty-journal-probe.log` reproduces the old behavior: existing empty destination, rename `Ok(())`, stage gone. The new Windows test `journal_publication_does_not_replace_empty_target_created_after_precheck` creates the competing directory inside the injected rename operation, then checks that the staged bytes and empty competing directory both remain. | +| Writer A stages before bytes X and after bytes Y, then waits to publish. Writer B completes the same Y and retires its journal. A publishes, detects that disk is no longer X, and previously entered rollback despite never writing. Since B's Y equals A's recorded after bytes, rollback could erase B's successful save. | New saves publish journal version 2 without a write marker. The first post-publication conflict retires only the journal. Before the first model write, the exact `write-started` marker is created and flushed. Explicit recovery of a valid v2 journal without this marker also retires only metadata, including when the initial retirement failed or the process stopped after publication. | `unwritten_journal_never_rolls_back_a_completed_writer_with_identical_after_bytes` runs the competing completed save before publication and covers both successful and interrupted retirement. `interrupted_publication_without_write_marker_never_rolls_back_external_matching_bytes` independently exercises reopening without a marker. Both verify exact persisted candidate bytes remain. | + +The first finding agrees with the current [Rust 1.98.1 rename documentation](https://doc.rust-lang.org/std/fs/fn.rename.html): supported modern Windows filesystems can use Unix-like empty-directory replacement semantics. The replacement primitive now follows the explicit flags contract of [Microsoft's MoveFileExW documentation](https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw). Paths used by production publication descend from the canonical project root; the reviewed raw UTF-16 conversion therefore retains the canonical Windows path prefix. It rejects interior NUL and keeps both terminated buffers alive through the FFI call. + +## Ownership and interruption checks + +The Windows retry loop performs at most seven publication attempts, with six delays totaling 390 ms. Only OS errors 5, 32 and 33 are retried. It checks for any destination object before every attempt, preserves the final OS error and journal path, and does not repeat model writes. Exhaustion leaves publication unsuccessful; it does not claim ownership of an existing journal. + +Recovery validates the journal version, entry, bounded before/after files, logical paths, marker bytes and directory contents before retiring an unwritten journal. A present marker must match the exact expected bytes; a link, nonregular file, oversized marker or malformed content cannot silently become permission to roll back. Version 1 journals keep their original rollback rules. With a valid v2 marker, the existing per-file before/after checks and interrupted rollback behavior remain in force. Atomic retirement precedes best-effort removal of fixed-name metadata files; cleanup does not recursively delete an untrusted directory. + +## Verification inspected + +The core owner's `.local/source6-core-recovery-tests.log` records 39 core library tests and 8 external source-file tests passing. The passing library cases include both publication races, bounded transient retry/exhaustion, missing-marker retirement, malformed-marker rejection, v1 compatibility, partial-save rollback and resumable interrupted recovery. These are inspected results from the core owner's run, not a second run by this reviewer. + +The core owner's `.local/source-save-probe.log` records 18 successful saves followed by two publication failures with Windows error 5. `.local/source-save-fixed-30-probe.log` records 30 successful saves and no remaining probe files. The inspected probe calls the production `new_project`, `compile_bundle` and `save_project`, compares saved bytes and bundle, and then removes its own generated entry. This is useful local reliability evidence, not a guarantee against arbitrary scanner lock durations or a multi-process stress test. + +## Limits retained + +- The module promises recoverability, not atomic visibility of a whole multi-file bundle or cross-process compare-and-swap. An unrelated process can still modify paths between checks and replacement; concurrent recovery of an actively writing journal is not a supported locking protocol. The marker means writes **may** have begun, not proof that this writer produced every matching after byte. +- Windows directory synchronization remains a no-op, as documented in the module. No power-loss durability claim follows from the flushed marker. +- An interruption during marker creation can leave partial marker bytes. Recovery deliberately rejects that malformed journal without changing model bytes; the test verifies this refusal. The fix does not provide automatic repair of corrupted metadata. +- New code reads v1 and v2 journals. An older executable that only accepts v1 will reject a pending v2 journal; this change does not promise downgrade recovery. +- The strict no-replacement primitive is Windows-specific. The unchanged non-Windows publication branch still uses `fs::rename`; this review does not claim that a racing empty destination is protected there. +- The 390 ms delay budget bounds sleeps, not filesystem-call latency. Longer-lived scanner locks or permanent ACL errors remain reportable save failures. Native save/reopen validation of the final integrated executable and the full workspace result remain separate release evidence. diff --git a/harness/release-linux-journal-publication.md b/harness/release-linux-journal-publication.md new file mode 100644 index 00000000..de2b0181 --- /dev/null +++ b/harness/release-linux-journal-publication.md @@ -0,0 +1,101 @@ +# Linux journal publication: refuse replacement + +Scope: the transition from a private staged journal directory to the pending +`.arcadia-save-ENTRY.journal` name. This is an ownership boundary before model +files can be replaced. It does not establish multi-file atomic visibility, +cross-process compare-and-swap, or power-loss durability. + +## Evidence and decision + +The actual Ubuntu 24.04.4 / kernel 6.8.0-139 / x86_64 / glibc 2.39 / ext4 probe +in `.local/linux-qa/rename-probe.py` showed ordinary directory rename replacing +an existing empty destination. Its output +`.local/linux-qa/runtime/linux-rename-probe.log` records replacement=true, +`RENAME_NOREPLACE` refusal errno 17, competing directory retained=true, and +fresh publication=true. That probe predates this Rust implementation. + +Linux now uses `libc::syscall(SYS_renameat2, AT_FDCWD, stage, AT_FDCWD, journal, +RENAME_NOREPLACE)`. The existing locked libc 0.2.189 package becomes a direct +Linux-only core dependency; no new package/version is introduced. Its supplied +architecture constants avoid handwritten syscall numbers. Calling `syscall` +also avoids adding a glibc 2.28 `renameat2` wrapper symbol requirement. + +The Linux manual documents no-replacement refusal, kernel and filesystem +support requirements, and EINVAL for unsupported flags. Kernel/ext4 support +begins with Linux 3.15; this is a requirement on this save operation, not a +claim that the whole application supports that old platform. +Sources: [rename(2)](https://man7.org/linux/man-pages/man2/rename.2.html), +[syscall(2)](https://man7.org/linux/man-pages/man2/syscall.2.html). + +Paths use original Unix `OsStr` bytes in live NUL-terminated buffers; embedded +NUL is rejected before the syscall. Failure preserves the OS error and journal +path. There is **no fallback**, retry, or substitute ordinary rename on Linux: +unavailable/blocked syscalls and unsupported filesystems stop the save before +the write-start marker or model writes. Windows retains MoveFileExW without +replacement and its existing bounded scanner retries. Other operating systems +explicitly retain their former standard-rename behavior; the stronger publication +guarantee is scoped to Windows and Linux. + +## Verification boundary + +Four Linux-only tests in `source_files.rs`, filter `linux_journal_publication`: + +- A competing empty directory, nonempty directory, file, or dangling symlink + appearing after precheck keeps its inode and contents; our staged metadata + remains intact and publication returns EEXIST at the journal path. +- Fresh publication preserves non-UTF8 Unix path bytes. Either path containing + NUL fails before moving the stage. +- Two synchronized staged publishers have exactly one winner. The other keeps + its complete stage and receives EEXIST. +- A competitor injected immediately before full-save publication prevents + ownership transfer, the write-start marker, and every model-file replacement; + captured original bytes and dependency expectations remain unchanged. + +An additional explicitly ignored Linux test requires syscall fault injection +and `OA_TEST_RENAMEAT2_ERRNO`. It rejects unconfigured execution and accepts only +ENOSYS, EINVAL, or EOPNOTSUPP. It exercises a full save and requires the original +errno/journal path, no ownership transfer or write marker, no pending journal, +and unchanged source bytes. Run the compiled core test binary directly under +strace (not Cargo) with `--ignored --exact +source_files::transaction_tests::linux_journal_publication_injected_unsupported_error_is_fail_closed`. +For example, set `OA_TEST_RENAMEAT2_ERRNO=38` and use +`strace -f -e inject=renameat2:error=ENOSYS TEST_BINARY ...`; repeat EINVAL/22 and +EOPNOTSUPP/95 on the x86_64 guest. Normal test runs report this test as ignored. + +Source formatted and `git diff --check` passed. The exact three-file delta was +copied into a separately hash-verified guest snapshot, preserving the original. +Actual Rust 1.90 execution on Ubuntu24.04.4 x86_64/glibc2.39/ext4 passed all four +normal focused tests (one fault oracle ignored), then all **42 normal library +tests** using the same built test binary. The ignored full-save oracle was +subsequently **explicitly executed and passed three times** under strace6.8: +ENOSYS38, EINVAL22 and EOPNOTSUPP95. Each raw trace confirms the actual +`renameat2(..., RENAME_NOREPLACE)` injected errno; all original-byte, no-write, +no-journal and exact-error assertions passed. Run: + +```sh +cargo test --locked -p oa-core --lib linux_journal_publication +``` + +Frozen implementation input SHA-256: + +| File | SHA-256 | +|---|---| +| `crates/oa-core/src/source_files.rs` | `669491DBDDB8E8E6556161426DFFA1A49AC1C826D31020BFF0956D81A4D84683` | +| `crates/oa-core/Cargo.toml` | `64824EC447BE777841A972BF06C894EF2624A37E4725583BCFE0389DEB9F00A3` | +| `Cargo.lock` | `A6E070ED59B0BA19F18D60E8104494B777B470BD0268AE348CBE2B229790449F` | + +Logs: `.local/linux-qa/runtime/linux-journal-focused.log`, +`linux-journal-all-and-injection.log`, and +`journal-injection-{ENOSYS,EINVAL,EOPNOTSUPP}.strace` in that directory. +The delta archive SHA-256 is +`3e57dad3be4c3c23b7952500924c41c5b671bcadb0e02a1d49d86f5bfa8f9300`; +all 226 original and patched snapshot file hashes were verified. Independent +read-only review found the current x86_64 syscall ABI and path-buffer lifetime +sound. The full library rerun includes existing interrupted-save and v1/v2 +recovery cases. [Linux environment and timing report](release-linux-validation-options.md). + +Remaining boundaries: complete current Linux integration/application/package +verification; other architectures/libcs/filesystems; network filesystems and +hostile ancestor-path mutation. These results do not establish power-loss +durability or model-file CAS. Journal retirement/recovery behavior was not +redesigned in this change. diff --git a/harness/release-rest-coverage-verification-20260906.md b/harness/release-rest-coverage-verification-20260906.md index bb71aad2..e8f0f76d 100644 --- a/harness/release-rest-coverage-verification-20260906.md +++ b/harness/release-rest-coverage-verification-20260906.md @@ -11,7 +11,7 @@ All **25 HTTP requests passed**. Evidence is retained under ETag metadata, binary provenance, server logs and `result.json`. The prepared runner is now tracked at `harness/fixtures/rest-coverage/run-coverage-probe.ps1`; the three-bundle payload is adjacent as `coverage-probe.json`. The tracked copy was -rerun successfully with another fresh store and the same clean PATH: all25 requests +rerun successfully with another fresh store and the same clean PATH: all 25 requests passed again under `.local/rest-coverage-20260906-161345`. See the adjacent README for reproducible invocation. diff --git a/harness/release-scope-reconciliation-20260906.md b/harness/release-scope-reconciliation-20260906.md new file mode 100644 index 00000000..969d3d3a --- /dev/null +++ b/harness/release-scope-reconciliation-20260906.md @@ -0,0 +1,162 @@ +# OA/SA scope reconciliation — 2026-09-06 + +Read-only production/specification review against the current user objective, +prior primary-source research and integrated branch evidence. No Cargo or native +input. Live reference checks used official Arcadia/Capella pages and the Eclipse +repository, not forum assertions or third-party feature lists. This is a bounded +requirements assessment, not certification of full Capella conformance. + +## Controlling interpretation + +The user requests a first release with “fully feature complete Operation Analysis ++ System Architecture, according to this application specification,” also asking +for **most** Arcadia semantics, a modern general systems tool, equivalent editable +graphical/text representations, deployable REST, artifact traceability and usable +built-in guidance. Later Arcadia layers are explicitly future work. The user does +not request every Capella implementation class, Eclipse extension, simulator, +native Capella file importer, or the full SysML metamodel. + +Consequently, `docs/scope.md` can document intentional semantics and limitations; +its “Studio profile” cannot itself authorize removing a material OA/SA workflow. +Conversely, an Ecore class without a discriminating engineering use case is not +automatically a new release requirement. Judge representation alternatives by +whether they preserve identity, context, editing and downstream meaning. + +R1–R6 of the original audit are substantially implemented: contextual topology, +mission/capability relations, iterative typed review, scenario reuse/correlation, +typed states/events and data/interface contracts. Source6 also supplies CM, +categories/selective views and coverage checks. The integrated branch audit and +current tests/reports supersede their original absence claims. Native and Linux +acceptance gaps remain gates; they are not missing metamodels. + +## Material unresolved semantics, in priority order + +### S1 — contextual assembly inside reused processes/chains + +**Recommendation: resolve before an unqualified feature-complete OA/SA claim.** +The official Capella guide explicitly describes assembling reusable chains via +involved functions that need not be the start/end, via a shared function, and +through nested references. This is process/functional analysis, not physical +deployment. [Official guide, Assembling Functional Chains](https://raw.githubusercontent.com/eclipse-capella/capella/master/doc/plugins/org.polarsys.capella.ui.doc/html/05.%20Diagram%20Management/5.5.%20Creation%20of%20Functional%20Chains%20and%20Physical%20Paths.mediawiki). + +Current evidence: `OccurrenceBody::SequenceReference` stores only a referenced +sequence ID; `RelationBody::SequenceLink` has direct source/target occurrences. +`validation.rs`'s `sequence_link_context` check requires equal direct owners. +`release_topology.rs::contextual_links_match_work_exchange_endpoints_and_reject_foreign_owners` +pins that restriction. Language section4.2 says the same; the process UI report +explicitly treats expanded reference endpoints as passive previews. This is an +intentional unsupported semantic boundary, not a lost branch implementation. + +**Minimal counterexample:** reusable procedure P contains contributions A→B→C. +An outer process reuses P twice, through R1 and R2, and also contains X. It needs +X to precede **B within R1**, while A within R1 may begin before X. R2 and the +shared P definition must remain unchanged. The relationship is contextual to R1. + +| Existing alternative | Why it does not preserve this particular intent | +|---|---| +| X→R1 as an outer SequenceLink | Orders the whole reused procedure after X, also delaying A. It cannot identify B. | +| Another outer occurrence referencing the same global work B | Avoids duplicating the work definition, but creates another contribution; it is not the B contribution within R1. | +| Copy A/B/C occurrences and links into the outer process | Can encode the flattened order, but loses reuse of P's occurrence/topology definition and its maintained updates. This is a different model, not an equivalent encoding. | +| Edit P to contain the X dependency | Changes the shared procedure and therefore both uses; the caller-local dependency is lost. | +| Put the condition in guard/prose | No maintained endpoint identity, context-specific reference cleanup, or equivalent structured navigation. | + +Two bounded alternatives deserve a contract comparison before implementation: +**(a)** a contextual endpoint containing a finite reference-occurrence path plus +leaf contribution ID, scoped to an outer owner; **(b)** explicitly declared +procedure connection points bound to internal contributions, with per-use +bindings. Both can retain shared definitions; (a) directly serves the inspected +Capella use case, while (b) introduces an abstraction/API authoring burden. Merely +allowing cross-parent IDs is insufficient because R1 and R2 share the same leaf ID. + +Pin the example in both OA and SA, including two uses, nested path resolution, +source edit propagation, deletion/reidentification, forward/reverse transition, +semantic review conflicts, exact source/API roundtrip and one Undo boundary. +Define common-function assembly separately: do not guess that two occurrences +execute once merely because they reference the same work. No production extension +is proposed as already accepted; another agent is independently falsifying this +counterexample and the alternatives. + +### S2 — operation identity in interface scenarios + +**Recommendation: demonstrate an equivalent current encoding or close the typed +reference gap before claiming complete interface-scenario support.** Capella's +official feature description distinguishes participant exchange scenarios from +interface scenarios whose messages identify interface operations. Its +Interaction schema derives a message's exact invoked operation from the message +ends. [Official features](https://mbse-capella.org/features.html), +[Interaction schema](https://raw.githubusercontent.com/eclipse-capella/capella/master/m2/plugins/org.polarsys.capella.core.data.def/model/Interaction.ecore). + +Studio supports Operation exchange items, interface allocation and participant +Call/Reply, but these are not proof of operation selection per message. +`OccurrenceBody::Exchange` currently has exchange, sender, receiver, kind, +reply_to and parameter values. Validation admits Interaction, FunctionalExchange, +BoundaryExchange or CommunicationMean as that exchange. The `BoundaryInterface` +viewpoint does not introduce a typed InterfaceAllocation/operation selection. + +**Counterexample:** one participant boundary channel offers two parameterless +operations, Reset and ReadStatus, through one interface. Two Calls need distinct +operation identities while retaining the same channel and participant pair. +Names alone do not follow operation rename/delete; there are no parameter values +from which to identify these zero-parameter operations. Selecting the entire +channel is ambiguous. A per-operation FunctionalExchange may be a legitimate +encoding **when those global functions/ports/flows already belong to the design**; +it is not generally equivalent if the engineer only specified an interface and +would have to invent a functional refinement or split the channel. + +The smallest next step is a core-only identity/lifecycle oracle for that example, +then an explicit operation reference or equally faithful mapping design. Freeze +whether the reference selects an interface allocation or its reusable item, how +provider/receiver and call mechanism validate, and how Reply inherits/correlates +the operation. Do not add a second scenario engine or infer an operation by name. +This is a demonstrated representational limitation; no failing runtime test was +executed in this read-only review. + +## Exclusions that do not establish additional blockers + +| Exclusion/choice | Assessment and condition | +|---|---| +| LA/PA components, implementation allocation, physical deployment | Explicit future layers. OA nesting and SA functional decomposition remain required and implemented; adding internal SA components would blur the chosen perspective. Prior primary research includes Capella's SA component validation rules. | +| Simulation, executable guards and hydraulic/control analysis | General modeling and descriptive constraints do not imply an execution engine. Keep labels honest; no claim of evaluated guard truth, timing proof or safety analysis. | +| Native `.capella`/`.aird`, full SysML, identical Eclipse metamodel/UI | No explicit interoperability requirement. Equivalent engineering outcomes matter; literal class/file compatibility is not required by “modern clone.” | +| Generic constraints instead of every temporal/configuration class | `SharedBody::Constraint { expression, constrained }` already attaches authored constraints to maintained semantic IDs; typed time/change events and scenario state observations also exist. Separate start/end timing observations and configuration-specific invocation can be richer, but this review has not established a concrete required use case that these existing descriptive facilities cannot faithfully express. Do not call them implemented, or call every Ecore difference a blocker. | +| No cross-layer data/item sharing; explicit traced counterparts | A deliberate identity/ownership choice can still satisfy both-layer modeling and review. Capella permitting upstream reuse does not require Studio to share the identical record across layers. Preserve intentional divergence and make copying/mapping explicit. | +| No semantic Git merge/LSP/autocomplete | Git-friendly commented modular text, syntax highlighting, linting and rejection are required and present. A merge engine or language server is not implied. | +| No collaboration/server history/autoretry/multi-replica service | Deployable querying/editing and conditional writes are required. Local Undo, ETags and explicit review can satisfy that boundary without collaborative merging or distributed storage. Actual Linux/deployment acceptance remains required. | +| No artifact fetching/plugin ABI/CAD execution | Maintained URI/revision/fragment and REST access meet the stated cross-tool traceability model without opening arbitrary resources or embedding plugins. Avoid implying external artifact contents are automatically validated or copied by Save As. | +| No signed installer, exhaustive accessibility/localization/performance promises | Not independently established as release requirements. A genuinely usable portable Windows/Linux distribution and representative supported-size interaction are required; missing runtime/lesson assets are blockers regardless of installer format. | + +## F2: behavior SVG export + +The user does **not** explicitly require SVG, report generation or faithful export +of every canvas. Exportability is nevertheless useful to systems engineers sharing +behavior with stakeholders. Architecture-only SVG is a defensible bounded product +choice if it is clearly labeled and does not export a different view under a +misleading promise. It is not a newly established OA/SA semantic blocker by itself. + +Current implementation evidence: `App::export_to` sends `active_diagram` to +`diagram::export_svg`; the visible Export SVG control is on the architecture view +toolbar, and no corresponding behavior export control was found. It does not +render the scenario/process/state workspace. The broadly worded scope/guide +description needs qualification; the root's F2 deferral is an engineering decision, +not an explicit user waiver and not a faithful behavior-export pass. + +If later acceptance demands sharing the visible behavior, compare a shared vector +scene/rendering adapter with an honest raster capture/export. Preserve exact +identity/labels/geometry for the chosen view. Do not silently repurpose the generic +architecture renderer or add behavior export solely to reproduce every Capella +menu. Current interactive behavioral editing and correct navigation remain the +higher-priority explicit requirement. + +## Required resolution + +Resolve S1 with the independent counterexample review and a bounded identity-path +contract; resolve S2 with an explicit alternative-encoding falsifier before choosing +its API. Correct current scope/help so “complete OA/SA” is not inferred from a +self-selected subset or from historical test totals. Continue the already assigned +native/Linux/package/end-state gates. This report adds no blanket demand for all +Capella variants and does not authorize reducing the user's requirement. + +Local evidence: [prior semantics research](../docs/research-v2-semantics.md), +[original release audit](release-semantics-audit.md), +[integrated branch audit](release-branch-integration-20260906.md), +[current native priorities](release-final-native-priorities-20260906.md). diff --git a/harness/release-selective-view-core-20260906.md b/harness/release-selective-view-core-20260906.md new file mode 100644 index 00000000..44ec6a5e --- /dev/null +++ b/harness/release-selective-view-core-20260906.md @@ -0,0 +1,78 @@ +# Saved selective view core — integration handoff + +2026-09-06. Implements the approved closed contract from +`release-selective-views-design.md`; this record does not claim desktop/native +acceptance or freeze source 6 before the integrating checks finish. + +## Implemented pure contract + +`crates/oa-core/src/view_options.rs` owns `ViewOptions`, all seven persisted +fields and their enums, category/options reference validation, owned `ViewIndex`, +prepared per-view predicates, accumulated omission reasons and exchange labels. +All defaults reproduce ordinary visibility with exchange names. Empty `Only` and +`Any` remain explicit empty selections through serialization and reference cleanup. +Selected categories combine by union within their own family. The two families, +global work coverage and chosen work context then intersect independently. +Participant exchanges, other relations and traces do not acquire work coverage +semantics. Labels use declared item identities; empty item lists say “No items”. + +`ViewIndex::new(&Project)` owns the accepted-model facts without a project lifetime; +cache it per accepted revision where useful. `prepare(layer, options)` computes +selected category unions and context reachability once. Each `includes(edge)` or +`reasons(edge, availability)` checks cached facts, never traverses the model. +No filtering helper creates placements, changes geometry or removes routes. + +Shared `all_behavior_evidence` / `behavior_evidence` walk explicitly listed owned +occurrences, branch membership and valid same-layer reuse. They do not walk every +semantic descendant or infer work usage from allocations, state actions or a +participant transport. Root-context guards and visited identities bound malformed +cycles. The core owner has replaced diagnostic `included_evidence` with this shared +helper, retaining its structural error gating and existing diagnostics regressions. + +## Required integration and evidence boundaries + +| Seam | Required behavior / owner | +| --- | --- | +| Model and admission | Core owner adds root-only ExchangeCategory and Diagram.view_options; strict pre-6 formats reject new fields/kinds instead of leaking live enums. | +| Lifecycle | Core owner calls visit_references / retain_references for hidden edges, categories and selected contexts; removing the last member never widens selection to All. | +| Semantic transitions | Category family/member facets require exact mappings; no identity guessing or implicit category membership propagation. Core owner integrating. | +| Projection | Core owner filters drawn edges after deriving automatic port layout from unfiltered eligible edges. Toggle-only edits must preserve nodes, ports, authored routes, nesting and action rows. | +| Omission browser | Pass real projection availability to prepared reasons. Missing endpoints, outside projection and folded internal edges must not be represented as explicit hides. Accurate projection availability belongs in core, not a parallel desktop implementation. | +| UI | Existing normal property draft/Apply/Undo/recovery must carry options. Separate family selectors, explicit empty choice, named identity search, all reasons and restore-hidden controls are needed. Root plus this worker's new forms/view_fields.rs integration. | +| Rendering/parity | Canvas, SVG and REST projection must use the same filter and item-label semantics. Source/project/JSON reopen must preserve settings. Root/core integration. | +| Dense diagrams | Forty exchanges with the same projected endpoints remain a separate readability gate for whether representation-only grouping is needed; these helpers do not claim to solve that visual problem. | + +## Focused evaluators + +Eight new core tests cover 12 independent family-selector combinations, 15 +coverage/context combinations, relation/trace and no-lines selections, immutable +hidden identity through category/label changes, all distinct projection omission +reasons, wrong-family/layer/unknown references, serialization/lifecycle empty modes, +and OA/SA nested/reused/unlisted behavior contributions including a malformed cycle. +The core owner's focused Cargo run passed all eight tests. Integration must +also run the existing coverage diagnostics suite and projection/lifecycle/source +round-trip regressions. Native Apply/Undo/reopen and dense display remain required. + +The new `forms/view_fields.rs` now implements the closed property editor using +ordinary `FieldShape` messages, with 24-record searchable pages and named choices. +Search/page position are ephemeral Draft fields, not model facts or recovery data. +Core `projection::edge_availability` records decisions in the actual drawing +pipeline; the desktop combines these with saved filter predicates. Semantic scoped +form browsing is mapped by the semantic UI owner. Desktop compilation and native +acceptance are pending. Existing property materialization may reject an edit while +another raw field is invalid; the raw draft is preserved, and this slice does not +claim independent scoped editing for normal properties. + +Independent integration review found that Traceability historically draws both +layers. Applying a declared-layer-only filter would silently remove the other +layer's lines under default options. Root approved the projection-specific fix: +`validate_options_cross_layer` and `prepare_cross_layer` admit hidden edges, +categories and contexts in both layers for Traceability; ordinary views retain +their declared-layer boundary. A ninth pure regression covers both-layer default +lines and mixed category/context/hide selection. The desktop uses the prospective +Diagram so pending projection/layer choices also determine picker eligibility and +omission geometry. This ninth test and two new property Apply/recovery/Undo tests +are compilation-ready for root's integrated run, not included in the earlier eight +passing tests. The two property tests additionally require that changing category +family retains incompatible members until explicitly corrected, rather than +silently dropping engineering membership. diff --git a/harness/release-semantic-artifact-boundary-review-20260906.md b/harness/release-semantic-artifact-boundary-review-20260906.md new file mode 100644 index 00000000..a4cad938 --- /dev/null +++ b/harness/release-semantic-artifact-boundary-review-20260906.md @@ -0,0 +1,20 @@ +# Independent semantic artifact GUI boundary review + +2026-09-06. Reviewed the artifact worker's proposed typed editor/recovery contract and existing core implementation before deep GUI wiring. This is a design/core boundary review, not acceptance of the unfinished GUI. + +Root ran the independent new `crates/oa-core/tests/semantic_artifact_absence_oracle.rs`: **2 passed, 0 failed**, each exercising both transition directions. Log: `.local/source6-artifact-absence-oracle.log`. + +## Established boundaries and corrections sent to implementer + +1. Presence observes attachment to the reviewed owner, not global record existence (`semantic/catalog.rs:381`). A still-existing source link moved elsewhere is absent from its old owner. Taking this explicitly identified absence removes the counterpart link without deleting or changing the moved source. The new oracle pins this behavior. +2. A missing source key is different from an identified absent link. `semantic/mutate.rs:418` deliberately preserves target-only data for TakeSource. The new oracle pins that no-op; the GUI must not synthesize a missing UUID to turn it into deletion. The reviewer's initial contrary hypothesis was falsified by this guard and corrected before the test run. +3. Creation needs an existing applicable source plus a stable reserved destination record and explicit decisions for Presence and all seven introduced metadata facets. A pair with neither initial artifact present cannot capture valid metadata observations: reviewed facets cannot be NotApplicable on both sides (`semantic/historical.rs:128`). Complete artifact candidates are not deletion tombstones: the final candidate must equal the final record, including selected metadata (`semantic/review.rs:1120`). The implementation worker owns additional direct creation/tombstone tests. +4. Presence aliases the actual Owner write slot (`semantic/review.rs:194`); the batch adapter must compare Value(owner)/NotApplicable, not merely Presence booleans. Deletion also removes the seven metadata slots. Cross-row Owner moves versus kept Presence and metadata versus deletion must produce visible retained conflicts, independent of row order. +5. Owner moves and simultaneous metadata edits are authorized on the immutable initially attached owner trace (`semantic/review.rs:609`), then recorded with the resulting owner. Further metadata review after Apply must use the current owner context; do not silently reinterpret the old trace. Existing `r3_semantic` owner/move/order tests already cover this core rule and were not duplicated. +6. Revision and Fragment distinguish None from Some(""); Name, Description and URI are strings. Optional toggles must preserve both raw text and enabled state through row switching and recovery. URI validation remains the existing nonempty URI-reference rule. Owner validity is the existing general model-object rule (`validation.rs:851`), not a newly invented same-layer or element-only restriction. Initial trace applicability is narrower than general artifact ownership; the GUI should expose resulting scope limitations explicitly. + +## Implementation review requirements still pending + +The proposed strict typed Saved DTO is appropriate if it preserves reserved IDs, raw optional values, exact selector decisions and row inclusion/order while validating duplicates and scope on restore. Candidates must be assembled from all chosen fields so the supplied complete record equals the core's final result; untouched seed fields cannot leak into unrelated edits. Invalid raw metadata must remain editable with a located error. Recovery must replan against the accepted model and preserve the ordinary source/history transaction path. + +The worker was sent these requirements directly. No production changes were made by this reviewer. New graphical adapter code, its recovery wiring, meaningful conflict rendering, undo/redo/source parity and native artifact workflows still require inspection/testing after implementation. Windows core test evidence above is not Linux, MSRV or release-package evidence. diff --git a/harness/release-semantic-artifacts.md b/harness/release-semantic-artifacts.md new file mode 100644 index 00000000..c0843f62 --- /dev/null +++ b/harness/release-semantic-artifacts.md @@ -0,0 +1,42 @@ +# Artifact lifecycle semantic GUI + +The artifact review slice is implemented. Its full desktop checkpoint passed **518/518**; the corrected artifact picker rerun passed **6/6**; strict all-target workspace Clippy passed. **MOVE, CREATE and DELETE passed native complete-source Save/Undo/Redo, with nine saves succeeding first attempt.** MOVE invalid raw/optional state and CREATE excluded row/reserved identity also passed exact controlled-stop recovery. Later forged-recovery tests and diagnostic/display changes have separate evidence and are not included in these checkpoint counts. + +## Contract + +- Name, Description, Owner, Relation, URI, Revision and Fragment have independent Keep/Take/Candidate decisions. Raw invalid Owner/URI input and deselected or excluded drafts remain retained. Optional revision/fragment store presence separately from retained text, distinguishing `None`, `Some("")`, and a disabled nonempty draft. +- Creation chooses an existing source link, explicitly reserves a destination identity/alias, then requires Presence and all seven metadata decisions. Reservation creates no accepted record. The request supplies one complete merged ArtifactLink candidate; copying seed values never silently reviews them. +- Deletion takes Presence from an explicitly identified link absent from its source owner. The source link may still exist under another owner. A missing source key means destination-only retention. No fake missing identity or record-shaped tombstone is invented. Fully new both-sides-absent metadata belongs to ordinary artifact creation, outside this semantic-review contract. +- Owner Candidate is authorized on the immutable initially attached owner trace. All core-admissible owner identities remain selectable. After Apply, further metadata review requires an appropriate trace at the new owner; the old trace is never silently rebound. +- Presence and Owner constrain the same actual Owner slot. Deleted records contribute NotApplicable to all seven destination metadata slots, so conflicting kept metadata or presence blocks the combined transaction. All included sources use one accepted base and one command. +- Exact artifact history resets are enabled. Reset-only changes observations, not link metadata; pairing uses exact identities rather than names or string inference. + +`semantic_review/artifacts.rs` owns closed raw values, exact-facet drafts, pairs, provisional seeds, lifecycle proposals and typed recovery. `artifacts_view.rs` provides named pairs, reservation, description, owner and optional-text controls. Row/session events retain their source/target/trace/facet address. Composition installs reserved candidates before comparing/writing actual slots. Recovery v1/v2 gain a default-empty artifact payload; no commands or proposed projects are serialized. No core production contract was changed. + +## Automated and independent evidence + +- Full desktop **518/518**: `.local/source6-artifact-inspector-desktop-tests2.log`, including six artifact tests and the diagram-inspector correction. The initial owned-Value closure compile error was corrected before this run. +- The fresh-editor picker regression first failed in `.local/source6-artifacts-final-tests.log` (5 passed, 1 failed). `Project::layer_of` follows elements/relationship endpoints, so filtering artifact IDs hid every ordinary artifact. Existing selected/history entries had masked the error. +- Corrected picker **6/6**: `.local/source6-artifacts-final-tests2.log`. It classifies by the artifact **owner’s** layer, admits neutral project/diagram owners, and shows current owner plus absence from the reviewed owner. A fresh empty Editor test proves same-side moved-link availability, opposite-side exclusion, and neutral-owner availability. +- Strict all-target workspace Clippy passed: `.local/source6-artifacts-final-clippy3.log`, after an equivalent `.map(RelationChoice)` cleanup. + +The six artifact tests pin direct-core creation/all-metadata, deletion/history, candidate-tombstone refusal and both-absent metadata refusal in both directions; GUI reservation/recovery; Owner/Presence conflict; None/empty and retained disabled text; invalid excluded raw recovery; moved-source deletion versus destination-only retention; and exact App source/history Apply/Undo/Redo/reset. These counts are separate evidence points, not a recomputed whole-workspace total. + +Independent state-contract and behavior reviews confirmed source-absence semantics, actual-slot comparison, record-candidate equality, immutable initial-owner authorization, raw optional recovery and owner admissibility. Both diagnosed the picker layer error. After its correction, no further material lifecycle defect was found in their bounded reads. The behavior reviewer identified adversarial forged-recovery admission as a useful additional test slice; those tests are separately owned. + +The [native lifecycle report](release-artifacts-native-20260906.md) records the actual Windows executable hash, usable owner/optional controls at 1440×900 and 1100×700, None versus empty preview, explicit Presence/Owner conflict rejection, and recovery of invalid Owner `-` with retained disabled Fragment text. Creation retained all eight decisions, seven metadata drafts and its exact reserved UUID while excluded through recovery; Presence alone could not apply. Deletion used an explicitly identified source link moved to another owner and removed only the destination. Each workflow's Apply/Save, one Undo/Save and one Redo/Save matched complete independent source oracles, covering metadata and review history. These runs predate the subsequent conflict-diagnostic and reserved-label corrections; revised presentation still requires current-binary verification. Destination-only retention remains automated evidence rather than a separately applied native case, and these fixtures do not establish every possible owner/type or Linux graphical acceptance. + +## Preserved fixtures and reproduction + +The tracked fixture directory is [`fixtures/release1-native/artifacts`](fixtures/release1-native/artifacts/README.md). It contains the source-6 move/delete/create bases, exact after source/JSON, full requests, identity/count manifests, relative-filename SHA256 inventory, native instructions and `generate-semantic-artifacts.rs`. No executable is committed. + +The generator requires `OUTPUT_DIRECTORY [RESERVED_CREATION_UUID]`. Both modes were compiled directly against the existing workspace core and validated without Cargo. Fresh generation checks all three variants. UUID mode reads the existing creation JSON and exact original source, preserves both base files byte-for-byte, and generates exactly the requested new artifact with eight acknowledged facts. The checked Windows compilation command is in the fixture README. + +For a copied native trial directory, regenerate the creation oracle using the GUI’s actual reserved identity: + +```powershell +. .local/toolchain/env.ps1 +.local/native-fixtures/generate-semantic-artifacts-preserved.exe .local/native-fixtures/artifacts-session +``` + +Move keeps two links and reviews three facets. Moved-away-source deletion reduces two links to one with eight acknowledged and zero retired facts because source metadata still exists. Creation adds one link with eight reviewed facts. All three have zero additional impacts. The generator validates direct core preparation and exact source-project round trips; it does not simulate native input. Work on copies and never regenerate initial identities after starting a native trial. diff --git a/harness/release-semantic-attachments.md b/harness/release-semantic-attachments.md new file mode 100644 index 00000000..0ee932cb --- /dev/null +++ b/harness/release-semantic-attachments.md @@ -0,0 +1,38 @@ +# Existing Child / Incident semantic review slice + +This slice wires the existing-record attachment adapters into the frozen-base batch review. Artifact-link lifecycle and creation of missing child/relationship records remain separate work. No core production changes were required. + +## Contract and implementation + +- `attachments.rs` owns explicit OA/SA identity pairs for Child, incident Source, and incident Target. Each exact facet address has an independent ordinary typed Draft; changing family, decisions, or active review row retains it. +- `row.rs` routes final Parent/endpoint edits to that actual record and exposes only the selected field. Attachment decisions are excluded from the owner-record constructor composition. +- `compose.rs` reads each attachment proposal from the same frozen base and compares its actual `(child, Header.Parent)` or `(relationship, Relation.Source/Target)` slot against all ordinary and attachment decisions. It emits one complete candidate per actual record, never sequential Apply commands. Two endpoint drafts can merge; conflicting direct Keep blocks while drafts remain retained. +- Taking source absence for an Incident requires an explicit final endpoint. A missing source identity retains a destination-only attachment; Child absence detaches only from the reviewed owner. Existing core checks remain authoritative for validity, induced impacts and history overlap. +- `attachments_view.rs` provides named pair pickers and scoped final-record controls with an 18px right gutter. The tab row already wraps; the new Attachments tab follows that behavior. +- `recovery.rs` adds default-empty attachment payloads to version 1 and version 2 and reconstructs typed drafts against the accepted base. It rejects duplicate addresses, changed identity/kind, wrong-layer IDs, or unrelated edited fields. Raw invalid selected fields survive. The separate source-5 upgrade adapter requires the externally verified legacy identity/fingerprint and clears prepared-impact approvals before normal restoration/replanning; ordinary restoration remains strict. + +## Verification status + +The four direct core attachment oracle tests passed before this GUI wiring (`semantic_attachments_gui_oracle.rs`, root log `.local/source6-core-recovery-tests.log`). The preceding desktop baseline was 483/483, including 82 semantic tests; strict all-target Clippy and frozen native GUI/server build passed. These results do not establish the new wiring. + +Nine new tests in `attachments_tests.rs` are wired and formatted but awaiting coordinated compilation/execution. They cover exact adapter/core parity for Parent and both endpoints in both directions, explicit Incident absence versus destination-only retention, retained picker families, malformed raw references and scope rejection, addressed GUI candidate parity, direct-record Keep conflicts, shared-relation endpoint composition plus excluded invalid recovery, exact App source/history Undo/Redo, and absent-payload/source-5 recovery bounds. Parameterized cases are not counted as separate Rust tests. + +Independent review was requested from the behavior reviewer. Native attachment acceptance remains pending. + +## Native recheck flow + +Use a fixture with an owner trace and mapped existing children/relationships. Open Review semantics from the owner trace, choose Attachments, then choose exact OA/SA child IDs and Use exact attachment pair. Set final value exposes the named destination child and its Parent only. Pick another compatible parent; the single combined preview must reflect the move without editing the source. Include the child trace and Keep its Parent to verify a conflict; exclude that row to retain its draft and restore the proposed move. + +For an incident relationship, select Source or Target explicitly and pair the relationship IDs. Set final value edits only that endpoint. If the source relationship is no longer incident to the reviewed owner, Take must remain blocked until an explicit replacement is authored. Review both ends of one relationship and verify one final record, one Apply, and exact Undo/Redo. Enter a malformed raw reference where available, checkpoint/recover, and repair it; rejected input must remain exact. + +Recheck tab wrapping, pair dropdowns, final field controls, history resets and all right-edge actions at 1100x700 and 1440x900. Verify saved project/history/source against direct core preparation, including unchanged comments and unrelated records. Functional-exchange port impact acceptance still needs a dedicated GUI/native falsifier in addition to these DataAssociation endpoint tests. + +## Coordinated test results and endpoint boundary + +Root ran the wired semantic suite: **92/92 passed**, log `.local/source6-attachments-semantic-tests.log`; this includes the preceding 82, nine attachment tests and the recovered trace-label regression. Source-5 recovery filter separately passed **13/13**, `.local/source5-recovery-tests2.log`. Existing trace choices now resolve their stored alias/name on recovery; missing IDs still use the deleted-reference label. + +An ignored standalone core probe established the precise FunctionalExchange rule: changing Incident Source alone keeps an output port owned by the old function and is rejected with `functional_port`; no automatic port impacts are prepared. Providing an existing matching output port through an explicit direct relationship SourcePort Candidate succeeds with zero impacts. The matching Source Candidate on that direct row agrees with the Incident actual slot and supplies the picker with its authored replacement owner. + +A tenth attachment test, `functional_incident_source_requires_explicit_matching_port_review_in_one_atomic_candidate`, was added after the 92-test pass and is pending its own execution. It checks the initial refusal, actual scoped picker availability, explicit SourcePort resolution, one merged relationship candidate/two reviewed histories, exact direct-core result and unchanged accepted model. It does not invent a port or implicit impact. + +The ignored native attachment fixture is validated under `.local/native-fixtures/semantic-attachments.*`, including exact before/after source and project JSON, request, IDs and hashes. `semantic-attachments-native.md` gives trace names and controls. Its two-candidate owner review has 12 elements, 2 relationships, 4 existing traces and no impacts. Native validation of the new attachment UI remains pending. diff --git a/harness/release-semantic-batch-implementation.md b/harness/release-semantic-batch-implementation.md new file mode 100644 index 00000000..bd8d964a --- /dev/null +++ b/harness/release-semantic-batch-implementation.md @@ -0,0 +1,78 @@ +# Semantic batch foundation implementation evidence + +Work begins from checkpoint `2b2d2bc`. This is an implementation ledger, not release acceptance. Root owns native input and coordinates the shared Cargo target. The semantic owner edits only `crates/oa-desktop/src/semantic_review.rs` and its subtree. + +## Current architecture + +`State` remains the App/Workflow facade, including `target`, `command`, `error` and recovery methods. `ReviewRow` stores independent raw scalar/constructor/keyed/order drafts. `batch.rs` owns the frozen Project, canonical shared references, stable source/target/trace addresses, retained row order/inclusion, active row and one combined plan. Row views emit addressed messages; facet identity accompanies draft edits. Exclusion affects request contribution without deleting raw input. Direction changes restore retained reverse drafts. Duplicate included source histories and reverse reference-map convergence are explicit conflicts. + +`compose.rs` derives target-slot intentions before creating the unique candidate per target. Keep is a frozen-value constraint, including explicit absence. Key pairs compare by destination identity; UUID-shaped annotation prose remains literal. Constructor discriminator equality alone is insufficient: constructor scopes also contribute their descendant catalog observations, and are compared against other rows' newly introduced slots. Existing membership/order adapters still supply row-local anchor rules; final implicit orders must agree with surviving combined membership. Only the session calls the core planner; row materialization does not require an individually valid Project. + +Recovery v2 stores shared session identity/references/accepted impacts once plus typed row payloads, explicit inclusion/order and active address. V1 imports through its prior strict decoder, including absent `orders`; no commands or proposed Projects are serialized. Aggregate structure/text bounds and retained-row limits supplement existing typed draft validation. Excluded invalid payloads remain editable after recovery. + +The root's observed reference-panel right gutter (18 px) is retained; the same gutter covers properties/detail, member, history and impact scroll content. Native rechecks remain required at 1100×700 and 1440×900. + +## Decisive regression matrix + +1. Two sources converge on one Integer target: first minimum 120 alone violates base maximum 100; second maximum 150 makes the atomic candidate valid. Compare one unique complete candidate and core-produced Project; reverse display order must not change it. +2. Two independent Candidate minima 120 and 130 disagree; both raw strings remain. Unselecting the second minimum restores the original first-row proposal. Equal values agree; Keep minimum 0 conflicts with either mutation. +3. Exclude an invalid `-` draft, apply another included row, then restore the excluded workflow without discarding that exact raw input. Switching focus, opening an existing excluded row and recovery must not implicitly include it. +4. Capture a text event from source A/minimum, switch to source B or another facet, deliver it: only its original row/facet may change. Also reject a genuinely unknown row address. +5. Constructor switch versus another source's kept retiring leaf, in both inclusion orders; constructor payload versus a newly introduced keyed member; first equal constructor-only acknowledgment with independent bounds. +6. Exact Annotation/Binding target keys with equal versus conflicting values, including UUID-shaped expression text, one-sided identity and presence removal. Different source-key pairs naming the same target slot cannot evade conflict comparison. +7. Ordered members preserve destination-only rows. Conflicting implicit anchor orders and combined membership without an explicit complete Order refuse; explicit repair remains row-authored, not silently inserted by the GUI. +8. Reverse distinct-target batch succeeds; reverse many-to-one mapping reports the existing core correspondence limitation without dropping a source. +9. V2 raw-row recovery and V1 migration, duplicate row/source/facet rejection, invalid active address, aggregate bounds and stale accepted-base rejection. Snapshot order/inclusion and all history resets survive. +10. Apply one batch through App/Document with exact source comments; one Undo restores all affected models/histories/source, Redo and reopen reproduce the same Project. Compare direct shared-core expected records, not merely the GUI's own intermediate serialization. + +## Validation status + +First production refactor checkpoint is saved and rustfmt parses owned files. Root's `cargo check -p oa-desktop --tests` is the next gate. Batch regression implementation and executable test results are pending; existing ordered regressions remain required. No native or release completion claim is made. + +## Integration corrections before executable validation + +The first root desktop test-target check found two extraction bridge errors (`workflow::RowEvent` and missing `form_event` import), plus the external data-fields test helper still using old row variants. These adapters are corrected; the shared test helper routes through an addressed row event. Category family/member selectors and CommunicationMean fixed constructor paths are integrated with the source-6 core contract. + +Native impact acceptance now carries the exact displayed `SemanticImpact`; a delayed acceptance cannot acknowledge a different current indexed impact. Typed row staging promotes its explicit correspondence into the shared session. Independent review found one reproducible recovery edge case: a rejected direction switch at the retained-row limit must roll back the direction. Its focused falsifier is prepared, with correction pending the current compiler edit freeze. + +The History tab's old `(0)` label counted pending resets, not persisted observations; the panel itself already rendered exact trace history without filtering by ephemeral references. The label now shows observation and reset counts separately. “Restore reviewed reference pairs” restores only exact persisted typed key pairs, atomically rejects conflicts, and excludes pending-reset observations. Order vectors contain no explicit correspondence map and are never zipped. Therefore unchanged order anchors absent from keyed history still require explicit correspondence. The new focused history regression checks this boundary and atomic refusal. + +Nine new regressions are installed (eight batch cases plus history restoration). Further exact-key convergence, category coupling and row-limit recovery falsifiers are prepared under ignored `.local` files for the next safe source window. No executable results are claimed yet. + +## Next bounded ownership after the batch gate + +Implement existing Child/Incident attachment review in `semantic_review/attachments.rs`, with changes limited to row payload/events, view, composition and recovery. The current composition address assumes every selected slot belongs to the trace target. Replace that assumption with a closed actual-record slot adapter: Child targets the mapped child's Header.Parent; Incident targets the mapped relationship's Source or Target field. This must compare against ordinary child/relationship review rows before merging one candidate per actual record. + +Keep an independent per-row attached-record draft bank. Parent choices and endpoint choices should name the concrete child/relation, current owner/endpoint, and proposed final owner/endpoint. Show explicit Operational/System key pairs. Child removal detaches only from this owner; an already differently owned target stays unchanged on a negative source observation. Incident removal offers an explicit final endpoint, not an unlink checkbox. The existing core Candidate branch reads a complete relation candidate's exact endpoint even when the owner trace is the only selected review row (`semantic/review.rs` Child/Incident Candidate branches). Opening another existing relation trace is useful for separate history but is not a necessary prerequisite for that candidate operation. + +The existing `r3_semantic::child_membership_and_parent_resolutions_agree_by_actual_child_slot` test is a direct oracle for Child/Parent agreement and order-independent refusal. No direct Incident-selector regression was found by searching the current core tests or source test names; the next slice should add an independent direct-core endpoint oracle before GUI implementation, covering both ends, both transition directions, absent Take refusal, Candidate reattachment, and conflict with a kept ordinary Relation endpoint. + +Artifact lifecycle remains a separate subsequent slice: typed provisional artifact identity, complete metadata draft, Presence plus all seven introduced metadata decisions on creation, initially attached owner context for metadata/move, and exact retired facts on deletion. This adds Artifact record read/install and independently addressed artifact slots to candidate composition. Reuse `artifact_creation_requires_reviewed_metadata_and_deletion_retires_exact_facts`, `artifact_owner_candidate_moves_link_and_conflicts_with_kept_attachment`, and `target_only_artifact_move_records_owner_and_metadata_in_either_order` as concrete core oracles. Add default-empty typed recovery fields with strict v1/v2 compatibility; do not persist plans or silently reinterpret prior snapshots. + +Second integrated desktop test-target check reported no semantic production errors; its only semantic diagnostic was the new exact-source test using `language::encode` instead of the existing `language::format`. That test call is corrected. Fourteen new desktop regressions are now installed. The row-limit switch is transactional for both event paths; independent review confirmed correction, bounded retained proposal memory and browser-only no-replan handling. Executable desktop results remain pending the coordinated shared Cargo slot. + +The next-slice direct core oracle is prepared in new `crates/oa-core/tests/semantic_attachments_gui_oracle.rs`: four parameterized tests cover owner-trace-only Candidate parent/endpoint replacement, absent Incident Take refusal with explicit Candidate repair, direct-record Keep conflicts on the same actual slot, and agreeing/conflicting convergent owner reviews under source/review permutation. Child and both incident ends run in both directions where correspondence is representable. This file is not yet executed; do not treat its assertions as established evidence until the target passes. + +## First executable integration run and correction boundary + +Root's first full desktop run (`.local/source6-desktop-tests.log`) completed **459 passed / 19 failed**. Failures exposed a shared constructor-observation issue: synthetic destination-only capture may legitimately be NotApplicable on both sides, whereas public historical `capture_facet` requires at least one applicable side. The adapter now checks only closed typed binding/collection applicability before capture; applicable malformed facts still receive core validation. First-equal Candidate constructor payloads also must survive composition so the core can reject unreviewed timer/default changes; complete candidate payloads and changed descendant intents are now retained rather than normalized away. + +The exact-source test was corrected to prepend its comment to the existing reachable source bundle instead of replacing only its entry with monolithic text. Stale errors explicitly identify the stale base. One pre-batch test previously mutated an external Project's history, refreshed against it, then expected application to become valid when a reset was unchecked. Root approved changing that assertion to the frozen-base contract: the uncheck, other decisions and presence values survive; no plan/proposed result or Apply is available until an explicit fresh review. This is a deliberate compatibility change, not an implicit rebase. + +The first attachment oracle run rejected the initial DataAssociation fixture because endpoints defaulted to Boolean data types. The fixture now explicitly uses Record definitions, including the added convergent source owner. This establishes no production defect. Focused reruns remain pending. + +### Attachment implementation seam after these gates + +`ReviewRow::combined_value` currently materializes only its owner record, and `compose::canonical` returns only a selector with the row target implicitly prepended. The smallest extension is `attachments::target_slot(owner, selector, direction)` and `ReviewRow::materialized_records(base, form_context) -> BTreeMap`: retain the existing owner composition, then apply each closed child-parent or relation-endpoint draft to its actual record. Candidate collection includes exactly those actual records having Candidate decisions. The session compares every resulting actual slot with ordinary Header.Parent/Relation endpoint decisions before creating the core's unique record candidates. + +Keep attached drafts keyed by exact facet address, not merely record ID, so independent source and target endpoint raw drafts do not overwrite each other. `candidate_draft`, scoped edit roots, and the Properties detail view delegate these two families to an attachment adapter. Pair staging stays explicit; related existing trace rows may be opened through current `OpenTrace`/row-bank behavior. Child Candidate uses a named final parent control; Incident Candidate uses a named final source/target control. This exposes the actual requested mutation rather than presenting an incomplete boolean unlink operation. + +Recovery adds a default-empty typed attached-draft vector to the per-row payload with duplicate-address, exact target record identity/kind and scoped-root checks. Parent/endpoint choices are user-authored values; preview, candidate records and plans are rebuilt against the original accepted model. No App/Workflow interface change is required for existing attached records. Explicit creation mappings and full artifact lifecycle remain later bounds and must not be simulated by reserving implicit children in this slice. + +## Focused semantic gate passed + +Root reran the entire `semantic_review` filter after the corrections: **82 passed / 0 failed** (`.local/source6-semantic-rerun.log`). This includes prior constructor/keyed/ordered/message-kind regressions, batch convergence and rejection cases, v1/v2 recovery, exact source/history Undo/Redo and the approved stale-base behavior. Full integrated desktop repetition and native batch QA remain separate gates. + +A standalone generator linked against built `oa_core-6c8da57367eec642` and its matching serde_json fingerprint `da2c4f333fc47818` created and validated `.local/native-fixtures/semantic-batch.arcadia`, initial/after JSON, independent core request and manifest. The exact after source comes from `source_project::update_bundle`. Three objects/two traces produce one target candidate, two acknowledged reviews and zero additional impacts; source and JSON roundtrips pass. Native steps are in `semantic-batch-native.md`, golden file hashes in `semantic-batch.hashes.json`. Initial source SHA256 `5e1f433e04621424edac43e53411624059e873f88dd531bfdb39093953485ed7`; exact after source `d3315bba762395205ae761e6e64029e8b4070c15a1299c6114075581db7b0974`. + +At root's request, the separate dense-view example was also compiled with direct rustc into ignored output and run: `.local/native-fixtures/dense-40/dense-40.arcadia` plus oracle. It reports semantic assertions passed, 46 elements / 42 relations, 40 routed edges, 12 ms, 0 label-overlap pairs and 151 exactly coincident path pairs. Those routing metrics do **not** establish native readability; root owns that acceptance check. No Cargo target files were mutated by these standalone generator builds. diff --git a/harness/release-semantic-conflict-diagnostics-20260906.md b/harness/release-semantic-conflict-diagnostics-20260906.md new file mode 100644 index 00000000..b60924f4 --- /dev/null +++ b/harness/release-semantic-conflict-diagnostics-20260906.md @@ -0,0 +1,71 @@ +# Semantic conflict diagnostics — 2026-09-06 + +## Current verification result + +The final combined diagnostic and reserved-label implementation is now verified on Windows with one Cargo build job. These results supersede the earlier interrupted/unrun statuses below: + +| Check | Result | Evidence | +| --- | --- | --- | +| `cargo test --workspace --locked semantic_review` | 108 desktop tests passed, 419 filtered; also two matching core tests passed; desktop runtime 72.71 s | `.local/semantic-diagnostics-final-tests.log` | +| Full desktop suite, using the exact workspace-compiled `arcadia_studio-58e30cee1fd51a8b.exe` test binary | 527 passed, none failed/ignored/filtered; 107.30 s | `.local/desktop-diagnostics-final-tests.log` | +| `cargo clippy --workspace --all-targets --locked -- -D warnings` | Passed, 44.99 s | `.local/diagnostics-final-clippy.log` | +| `cargo build --workspace --bins --locked` | GUI/server build passed, 38.64 s | `.local/diagnostics-final-build.log` | + +The new binaries were copied, not launched, into `.local/native-qa/bin-source6-diagnostics`. That directory contains exactly the GUI, server, and `libunwind.dll`. The running native process 2540 was untouched. SHA-256: + +- `arcadia-studio.exe`: `2D183AAA66445E002AB1FBADBC532FE99AF97B3C62937CB018901B4F9E84B147` +- `arcadia-server.exe`: `1F98F02DBC9FB2D9AFA3C42BB0CB4038BAAE81C85EF17365B5CB486669E749F8` +- `libunwind.dll`: `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4` + +Hash evidence: `.local/diagnostics-staged-sha256.txt`. Native verification of the corrected labels remains root-owned and pending at this handoff. This is not a new full-workspace test pass: only the semantic filter and complete desktop suite ran, followed by workspace Clippy/build. Source files stayed frozen through the chain; deferred guide issues below are not included. + +## Finding and implementation history + +Native artifact review exposed a misleading diagnostic: Keep Presence plus an Owner move in the same review was described as disagreement between independent rows. Its actual-slot selector also rendered an artificial missing side, and owner values appeared as raw JSON UUIDs. + +The change preserves the originating reviewed facet alongside each proposal's actual mutation slot, for diagnostics only. Same-row conflicts now start “Decisions in this review disagree”; distinct rows retain “Independent row proposals disagree.” Messages identify both original facets, source/target/trace aliases, and named artifact owners. UUID-shaped URI/text values remain literal text. Actual slot equality, proposal comparison, candidate composition, and commands are unchanged. Synthetic metadata retirements retain their originating Presence decision. + +Three focused tests passed under the Windows workspace feature graph: + +```powershell +. .local/toolchain/env.ps1 +cargo test --workspace --locked diagnostic_tests +``` + +`.local/semantic-conflict-diagnostics-tests.log`: exit 0, three passed, 522 desktop tests filtered, 0.16 seconds test runtime. Tests cover the real State path for Keep Presence versus Owner Candidate, retained drafts and successful explicit repair; distinct traces with equal display names; and typed Owner versus UUID-shaped text/absent/optional-value rendering. + +The subsequent `cargo test --workspace --locked semantic_review` was **interrupted, not passed**, at the root's host-memory guard while the Linux build was active. Compilation had finished; no assertion failure was observed before Ctrl+C. `.local/semantic-conflict-diagnostics-regressions.log` retains the partial run, exit 1. Free host memory recovered to 1,770,632 KiB after stopping, and no Windows Cargo/rustc/test process remained. Strict Clippy and GUI/server builds had not started. Resume broader checks only when root grants resources. + +Native verification of the revised wording is pending; parent native evidence was obtained from the preceding binary. + +## Reserved artifact labels + +A subsequent native creation review correctly labeled the fresh destination in its picker, but the facet header showed “Deleted reference” for the same reserved UUID. Facet titles and reference-pair displays used the accepted-project-only resolver and therefore lacked the row's reservation context. + +The bounded presentation fix adds row-aware facet titles and identity choices, backed by the existing artifact seed name/alias. Facts list/detail, history captions, reference pairs/selected controls, and existing reference-choice label mapping use that resolver. Truly missing identities retain “Deleted reference”; an accepted artifact takes precedence over a retained reservation. No picker membership/filter policy, stored DTO, creation decision, or accepted model changes. + +`view_reserved_artifact_tests.rs` exercises both directions through normal Reserve/Stage events and stored JSON restoration, then checks reserved headers/reference labels, exact snapshot retention, missing-identity fallback, accepted-record precedence, and unchanged accepted model. **This new test is formatted but unrun**, pending root's Windows Cargo resource grant. Native observations describe the previous binary; revised labels require later native verification. + +Independent review found a residual diagnostic route: Take Presence plus Keep destination Owner for a reserved destination still passed through accepted-base-only identity/facet formatting. The conflict formatter now receives the immutable review State and resolves each originating proposal through its exact RowAddress. Accepted identities retain precedence; a row can use only its own reservations. This is display context only; slot keys and observation equality remain unchanged. + +Added `reserved_creation_conflict_uses_its_rows_seed_instead_of_a_deleted_reference` to the diagnostic module. It authors that partial-creation conflict in both directions, repeats after exact recovery, requires seed name/alias rather than Missing/Deleted labels, verifies unknown-row lookup cannot borrow the active reservation, and checks the accepted model stays unchanged. **This fourth diagnostic test and the latest formatter revision are unrun** while Linux owns heavy build resources; the earlier three-test pass does not verify this later change. + +## Deferred minor guide wording + +During the later frozen verification chain, root observed that First Project step 06 says “Save project,” while the current toolbar says “Save all.” Root judged the minimum-size guide layout usable and requested deferring this wording correction to preserve the running checks' source boundary. No guide source change was made in this diagnostic slice. + +Root then found a navigation defect at 1100×700: open Scenarios through search, scroll its article to the bottom, and select related “Exchanges & dataflow.” The sidebar changes correctly, but the new article retains the old scroll offset, hiding its heading/introduction. The next guide slice should reset article scroll on a changed topic (including a search-selected topic), while preserving same-topic search behavior. This report records the observed reproduction; no guide source fix is included in the staged diagnostic binaries. + +## Subsequent guide fix and final staging + +The separately authorized guide slice is now implemented: the article scrollable has the explicit `engineering-guide-article` identity; State reports whether its topic changed; the App event sends an article-only snap-to-start when it did. Explicit related-topic navigation and search auto-selection of another topic reset scroll. Same-topic search, an empty-result query, clearing that query, and reselecting the same topic do not schedule a scroll reset. First Project step 06 and the Ctrl+S reference now say “Save all.” Existing Fill/wrapping layout constraints remain. + +`cargo test --workspace --locked guide::tests` passed all three tests, 525 desktop tests filtered, 0.06 s runtime (`.local/guide-navigation-tests.log`). The new test runs actual App messages and consumes the emitted Iced widget operation against article and unrelated-scroll probes; it checks exact start offsets and accepted model/source/revision/Undo invariance. Strict workspace all-target Clippy passed in 13.11 s; workspace GUI/server build passed in 14.72 s (`.local/guide-navigation-clippy.log`, `.local/guide-navigation-build.log`). This localized follow-up did not repeat the full 527-test desktop run above; the new test brings compiled desktop inventory to 528. + +Final guide-stage directory `.local/native-qa/bin-source6-guide` contains exactly: + +- GUI SHA-256 `A5A741349B49D0C90A6BDB0FA7871415133CBCC1B8A4ECF0C4EDDDB99313F2D2` +- Server SHA-256 `1F98F02DBC9FB2D9AFA3C42BB0CB4038BAAE81C85EF17365B5CB486669E749F8` +- `libunwind.dll` SHA-256 `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4` + +`.local/guide-staged-sha256.txt` preserves hashes. Binaries were staged, not launched; the running native app was untouched. Root still needs native verification of the fixed Scenarios→Exchanges related-link scroll and same-topic search behavior, plus diagnostic/reservation wording using this latest binary. Windows Cargo lease was released after staging. diff --git a/harness/release-semantic-review-navigation-20260906.md b/harness/release-semantic-review-navigation-20260906.md new file mode 100644 index 00000000..931eb168 --- /dev/null +++ b/harness/release-semantic-review-navigation-20260906.md @@ -0,0 +1,27 @@ +# Semantic-review counterpart navigation finding + +Status: native defect confirmed on `F5ECAEC…`, corrected and reverified on `19180FE…`. Focused tests, strict Clippy, binary build and exact native model/source/history checks pass. + +At 1100×700 in the teaching lesson, root renamed OA `@oa_confirm`, saved, then applied one semantic Name Take decision. The application selected SA `@sa_confirm` and changed its layer to SA, but retained the OA allocation canvas. The diagram picker displayed **Choose diagram** because its available choices were filtered to SA while `active_diagram` still identified OA. A further snapshot did not repair the mismatch. The retained explorer query `@oa_confirm` naturally had no SA result; query preservation alone is not the defect and should not be removed as a workaround. + +`app.rs` Semantic Apply success (around lines 1840–1850) sets `selected`, `layer` and refreshes the property draft, without assigning `active_diagram`. `execute()` calls `sync()` before this block changes the layer, so its compatibility normalization still sees the previous OA layer. `views.rs` reads the active UUID for the canvas but filters the picker choices by current layer. The ordinary transition branch already chooses a target-layer diagram and therefore provides a bounded reuse seam. + +Proposed smallest correction: a shared post-commit focus helper updates the accepted target selection, clears the stale placement selection, sets its layer when available, and chooses a compatible diagram using the existing preferred-diagram policy. Refresh the draft after this context is consistent. Keep the camera when the view/layer did not change; fit the architecture only on a real destination change. Callers retain their page/behavior policy, so this does not broaden into an unrelated navigation redesign. Use it from semantic Apply and the ordinary transition's equivalent selected/layer/diagram block. Do not mutate saved presentation, create nodes, call `reveal_port_owner`, clear search, or perform a second model command merely to navigate. Do not call guarded `select()` blindly: its same-selected-ID early return can skip repairing the layer/view mismatch. + +Meaningful focused tests should execute an actual semantic Name Take in both directions with distinct OA/SA views; assert target identity, matching layer/view, retained search and exact core/source single Undo/Redo. Cover no destination-layer diagram (clear active view rather than show the old layer), repeated same-view focus retaining its camera, rejected source writeback preserving the original review/navigation, and existing behavior-transition routing. A root-owned native rerun must repeat the teaching step on the rebuilt binary and observe the SA counterpart in its intended view. The earlier exact semantic/source history oracle does not close this UI defect. + +Root approved the bounded correction. `app.rs` now has `focus_after_review(target, destination)`, called by semantic Apply and the ordinary transition post-commit selection block. It retains an already visible target view, otherwise resolves a compatible target-layer view (or None), clears the obsolete node selection and refreshes the property draft. Semantic Apply fits only on an actual layer/view change. Callers retain their existing workspace policy; no presentation/model command was added. + +Four dedicated tests in `app/review_navigation_tests.rs` cover actual Name Take both directions with complete accepted core equality and exact one-Undo/source history; missing destination view without view creation; retained same-context camera/query; and rejected source writeback retaining review/navigation. Existing transition-context tests remain unchanged. Rustfmt and scoped diff-check pass. + +Root's first focused run compiled and passed three tests; the fourth expected no selected placement, but `refresh_draft()` correctly selects the accepted counterpart's placement in its new view. The test-only correction requires that the selected placement belongs to the destination view and refers to the exact counterpart, and is absent from the old source view. Root's fresh rerun passed **4/4**, 535 filtered, 1.49 seconds runtime after a 36.50-second test build (`.local/semantic-review-navigation-tests2.log`). Original failure evidence remains in `semantic-review-navigation-tests.log`; no production change was needed after that run. Further strict checks/build/native revalidation are root-owned and not inferred from this focused pass. The existing `F5ECAEC…` stage does not contain this fix. + +Strict Clippy initially found one collapsible nested if in the new call site. The equivalent let-chain correction retained helper execution before checking the page. Fresh `cargo clippy --locked --workspace --all-targets -- -D warnings` passed28.26s; `cargo build --locked --workspace --bins` passed50.50s, each one job (`.local/semantic-review-navigation-{clippy2,build}.log`). No full539desktop test run is claimed. + +Root's native replay used process3012, `.local/native-qa/bin-source6-review-navigation`, GUI SHA256 `19180FEAB9E80C0CD8DB128E281DF353E74BF966ED9DB8D9CD8556A5EEBCD6C5`; server/runtime hashes remain those in the teaching native report. At1100×700, opened a whole-folder copy of the preserved OA-only rename in `.local/native-qa/water-pressure-navigation-working`. Selected exact `@oa_confirm` through search, opened Review semantics, chose Header Name Take Operational, confirmed one decision/zero resets/zero additional impacts, then Apply. + +The first post-Apply snapshot immediately showed **2 · SA service boundary and functions**, SA perspective and selected Function `@sa_confirm` with `Confirm stable pressure`. The System remained Pressure coordination service. The preserved OA search naturally had no SA results; the canvas and picker were coherent. Save all succeeded on first submission. The independent generator/core/source checker matched the complete Project and all30source files without normalization. All37files matched the earlier accepted semantic result exactly, proving navigation did not alter presentation/source facts. + +One Undo retained the coherent SA view and restored Confirm safe pressure; Save first submission restored all37files exactly to the OA-only baseline. One Redo retained the view and restored Confirm stable pressure; Save first submission restored all37accepted files exactly, and the full model/all30source checker passed again. Evidence: `.local/native-qa/water-pressure-navigation{,-undo,-redo}-hashes.json`, checker output directories `water-pressure-navigation-oracle` and `water-pressure-navigation-redo-oracle`. The first checker launch without toolchain PATH produced no result; the properly initialized rerun explicitly returned PASS/exit0, so only that rerun is acceptance evidence. + +Three native saves succeeded first submission. No recovery, forced stop or new diagram was part of this replay. The bidirectional/missing-view/same-camera/rejected-writeback variants remain focused automated evidence; the exact originally failing OA→SA scenario now has actual native acceptance. diff --git a/harness/release-source5-checkpoint-compatibility-20260906.md b/harness/release-source5-checkpoint-compatibility-20260906.md new file mode 100644 index 00000000..419922b1 --- /dev/null +++ b/harness/release-source5-checkpoint-compatibility-20260906.md @@ -0,0 +1,33 @@ +# Source-5 private checkpoint compatibility + +Implemented and verified by focused Windows automated tests on 2026-09-06. Native source-5 recovery, Rust 1.90 and Linux verification remain pending. This note does not claim release acceptance. + +## Defect and selected boundary + +After the source-6 upgrade, private checkpoints written by the source-5 desktop were still listed on Home but Recover rejected them: compiling their accepted source changed its header, violating the former exact-bundle guard. Removing that guard alone would also leave workflow fingerprints bound to the old serialized model and could admit identity insertion or broader migrations. + +Recovery now accepts either an unchanged current bundle or a proven lossless source 5 to 6 upgrade. It uses the real language token spans and requires the complete migrated bundle to equal the original bundle with only its significant entry version token changed from `5` to `6`. All other bytes, IDs and file membership stay identical. Older schemas, malformed/future source and missing identities still fail explicitly; checkpoint and origin files are retained. This private compatibility path does not weaken frozen source-5 admission of model constructs. + +The recovered model remains a detached, dirty local copy without fabricated undo history. `app/checkpoints.rs` installs the migration report and explains the upgrade and review requirement. No model file is written by Recover. + +## Raw drafts and workflow identity + +- Clean editor buffers refresh only when both text and baseline exactly match the old accepted file, the workspace is applied, and its entry matches. Dirty text AND baseline, detached buffers, unrelated clean buffers, unapplied invalid-open workspaces, active file and new-path draft remain unchanged. +- Property and behavior forms retain raw authored DTOs. Old Diagram property values without `view_options` remain editable through the existing defaulted model field. Invalid numeric input remains invalid rather than being normalized or discarded. +- A private historical serializer reconstructs the source-5 Project/Diagram field order from commit `2b2d2bc`: schema version 5 and no Diagram.view_options. It is used only after the exact token-upgrade proof. An independently pinned literal and FNV-1a value verify field order, including a diagram; arbitrary JSON key rewriting is not used. +- Typed relation, trace, transition, port and semantic adapters require the saved legacy fingerprint to match that proven accepted model. Semantic adapters additionally check model identity. They restore raw choices and recompute current previews. Semantic v1/v2 impact approvals clear and must be reviewed again. Stored commands are not part of the checkpoint contract and are never replayed. + +Implementation: `crates/oa-desktop/src/recovery.rs`, new `recovery/source5.rs` and `source5_tests.rs`, `source_workspace.rs`, `workflow/recovery.rs`, `port_workflow.rs`, `semantic_review/recovery.rs`, and `app/checkpoints.rs`. No Document or model schema changes were needed for this compatibility fix. + +## Evidence + +- `.local/source5-recovery-tests2.log`: **13 passed, 0 failed** for the `source5` filter, covering exact header/comment/CRLF/Unicode preservation; clean and dirty baselines; distinct unapplied workspace; unchanged checkpoint/origin bytes; explicit source-4 and missing-ID refusal; transition/port stale-base rejection and rebinding; invalid property text; old Diagram property shape; unrelated clean buffer preservation; pinned historical fingerprint; semantic v1/v2 identity/raw-state preservation and approval reset. +- `.local/source6-attachments-semantic-tests.log`: **92 passed, 0 failed**, including the semantic recovery adapter and integrated review regressions. +- The first desktop test compilation passed. Its initial source-5 fixture incorrectly used unsupported block comments, causing nine parse failures. The fixture now uses supported `//` comments between header tokens; production already filtered the actual tokenizer's Comment tokens. The accepted-upgrade policy was not relaxed. +- Existing code was frozen after the fixture correction. No Cargo was run by this worker; root coordinated both recorded runs using the prepared Windows gnullvm toolchain. + +## Remaining native and platform checks + +The task-local native fixture is `.local/source5-native-recovery-fixture/15000000-0000-4000-8000-000000000010.json`, generated by `.local/make-source5-recovery-fixture.py`. It has two accepted source-5 files with explicit identities, exact comments/CRLF/Unicode, an invalid dirty entry with its original baseline, and an unfinished new-file name. It has not been installed into user recovery storage by this worker. After the new desktop build, verify Recover's migration notice and exact raw editor text; repair and Apply explicitly, then Save As and compare the accepted source oracle. The previously frozen source-6 binary predates this fix and is not evidence for it. + +Automated evidence is Windows gnullvm only. No MSRV or Linux build/runtime claim follows from it. WSL remains pending the user-authorized Windows restart; no import was retried. The separate journal publication implementation guarantees no-replace on Windows via MoveFileExW. Unix fs::rename still has the documented competing-empty-directory limitation. A future Linux-specific renameat2(RENAME_NOREPLACE) path should fail closed on unsupported kernel/filesystem responses and be tested with an injected empty destination on the actual Linux filesystem; this remains a proposal, not implemented portability evidence or a general model-file compare-and-swap guarantee. diff --git a/harness/release-source5-checkpoint-independent-review-20260906.md b/harness/release-source5-checkpoint-independent-review-20260906.md new file mode 100644 index 00000000..62b6614a --- /dev/null +++ b/harness/release-source5-checkpoint-independent-review-20260906.md @@ -0,0 +1,16 @@ +# Independent source-5 checkpoint review — 2026-09-06 + +No new material defect was found in the bounded source-5 checkpoint upgrade. This was a read-only review of implementation owned by other agents: `recovery/source5.rs`, `recovery/source5_tests.rs`, `recovery.rs`, `source_workspace.rs`, `workflow/recovery.rs`, `port_workflow.rs`, `semantic_review/recovery.rs` and `app/checkpoints.rs`. No Cargo or native operation was performed by this reviewer. + +| Concern | Inspected behavior | +| --- | --- | +| Admission | Restore first compiles the accepted bundle. Compatibility requires the reported migration to be exactly 5→6 and the resulting entire bundle to equal the original after replacing only the parsed entry version token. Comments, whitespace, includes and UUIDs must otherwise remain byte-identical. Missing identity insertion, other source changes and source 4 are rejected. | +| Raw buffers and baselines | Only an applied workspace with the same entry can refresh clean buffers, and only where text equals baseline equals the old accepted file bytes. Dirty invalid text, its old baseline, an unrelated clean buffer, and an unapplied alternative workspace remain untouched. | +| Historical model binding | The legacy serializer preserves the historical Project/Diagram field order and omits the new default view options. Its shape was compared against the model changes since checkpoint `2b2d2bc`; new constructors do not change the existing serialized variants. Old workflow fingerprints must match this independently reconstructed accepted base before rebinding to schema 6. This is a consistency check, not cryptographic authentication. | +| Workflow intent and approvals | Typed relation/transition/port inputs are restored through their ordinary restore/replan paths. Semantic v1 and v2 retain authored choices and raw drafts, clear accepted impact approvals, then use strict current restoration and planning. No serialized executable command is accepted as a restored plan. | +| Limits and scope | The normal 256 MiB checkpoint read bound, source workspace validation, typed unknown-field rejection, semantic aggregate draft limits, stable row-address checks and row-count bounds remain in the restore path. The compatibility branch does not bypass them. | +| Failure and destination | App prepares the complete restoration before resetting the current document. A failed upgrade retains the checkpoint and existing document. A successful restore is dirty with no undo stack or original/hosted destination; source files and the checkpoint are not rewritten by migration. | + +Inspected `.local/source5-recovery-tests2.log`: **13 tests passed**. These cover historical serialization, exact token-only migration, invalid raw text/baselines, unrelated/unapplied workspaces, untouched origin/checkpoint bytes, unsupported source 4 and missing identities, old Diagram property shape, stale workflow/port fingerprints, and semantic v1/v2 compatibility with approval clearing. The later `.local/source6-desktop-attachments-recovery-all.log` records **506 desktop tests passed**. These are other owners' execution results, not independent reruns by this reviewer. + +The review does not establish native old-checkpoint migration, arbitrary-corruption repair, power-loss durability or every historical binary's private checkpoint format. Unsupported or malformed records deliberately fail without replacing accepted work. The running source-6 native binary predates this upgrade; new-binary native restore/repair/Save As remains a release gate. diff --git a/harness/release-source5-native-recovery-20260906.md b/harness/release-source5-native-recovery-20260906.md new file mode 100644 index 00000000..4fba5808 --- /dev/null +++ b/harness/release-source5-native-recovery-20260906.md @@ -0,0 +1,44 @@ +# Native source-5 checkpoint recovery — 2026-09-06 + +Windows process 9716, window 5702656, staged desktop +`.local/native-qa/bin-source6-recovery/arcadia-studio.exe`, SHA256 +`DD5D01FA07B921F1A7B365C33867876A7AE1BCBD01815B4BB4206408D2012F95`. +Isolated APPDATA/LOCALAPPDATA, system-only PATH plus colocated libunwind.dll. +This build includes source-5 recovery, Child/Incident review and visible-line +counts. It precedes artifact reconciliation and the diagram-inspector fix. + +The synthetic checkpoint `15000000-0000-4000-8000-000000000010` was installed +only in the task-local recovery directory. Home Refresh discovered it; Recover +opened Textual model with the original invalid source-5 draft, its Unicode text, +two-file workspace and pending filename `drafts/retained-name.arcadia` visible. +The footer explicitly reported recovery of source 5 as source 6 in an unsaved +copy. The invalid token diagnostic remained visible; no silent repair occurred. + +The new retained checkpoint `7d754698-fcae-4e95-b38a-d49b77292916`, copied to +`.local/native-qa/source5-recovered-before-repair.json`, proves exact equality +of the dirty raw text, its old baseline, included source and pending filename. +Only the accepted entry's parsed version changed to 6. The original checkpoint +remained byte-identical, SHA256 +`71CCA2A59F36B2A07D11DEDBE9101226AA1A4A00D8AC244B5565202E56709F51`. + +Native repair inserted `// ` before the unfinished last line, preserving that +text as a comment. Apply all accepted the bundle and changed the draft's version +token to 6. The two advisory coverage diagnostics for the fixture's unallocated, +unused activity remained visible. Save as to +`.local/native-qa/source5-repaired/recovered.arcadia` succeeded on the first +attempt, renamed the entry, retained `work.arcadia`, and displayed All changes +saved. Both original and recovered private checkpoints retired (zero JSON files). + +Direct post-save comparison proved the entry equals the initial raw draft with +exactly the version-token substitution and comment insertion. All seven CRLF +separators, Unicode, header comment, stable IDs and included-file text survived. +Saved hashes: + +- `recovered.arcadia`: `2D6F09D94395928EF892EFF0DDBF257E939CF5B9B8F82FC594309EEF30036231` +- `work.arcadia`: `10D0D6B7EAEB068F9D99206495AB5E2A4D7D935DEBBE6A0940BB61A47D7156C2` + +This proves native recovery/repair/save for a deliberately constructed old +checkpoint. It does not claim an old-version process crash was performed, or +that every historical checkpoint is migratable. The strict admission and +workflow fingerprint boundaries have separate automated and independent review +evidence. The first-release goal remains incomplete. diff --git a/harness/release-source7-integration-decision-20260906.md b/harness/release-source7-integration-decision-20260906.md new file mode 100644 index 00000000..497b0807 --- /dev/null +++ b/harness/release-source7-integration-decision-20260906.md @@ -0,0 +1,29 @@ +# Source7 integration decision — contextual chains and operation messages + +Root decision after executable existing-core falsifiers, distinct candidate comparison and independent critiques. These are approved implementation contracts, not a claim that production support exists. Original first-release OA/SA scope remains unchanged. + +## Selected representations + +S1 uses the new closed ContextualLink relation with an explicit caller owner, endpoint paths of reference **occurrence** IDs and a direct leaf, and distinct Precedence/Exchange/CommonWork purposes. Existing direct SequenceLink bytes and meaning remain unchanged; all-direct new Precedence rejects in favor of the existing constructor. Same global leaf in R1/R2 represents distinct contributions. CommonWork asserts shared work identity, without execution fusion. The [contract/probe](release-chain-assembly-contract-probe-20260906.md) and [independent critique](release-chain-assembly-contract-independent-review-20260906.md) define the admitted cases and required owner/reference-consumer audit. + +S2 uses `operation: Option }>` on the existing Exchange occurrence. None retains ordinary channel semantics; present/unresolved preserves operation intent; present/selected references the exact InterfaceItemAllocation. Reply stores None and derives operation/provider/signature from its exact Call. A name, unique parameter list, bare item ID or fabricated functional refinement is not a substitute. The [operation contract](release-interface-operation-contract-20260906.md) and [independent critique](release-interface-operation-contract-independent-review-20260906.md) supply the primary evidence and original parameterless falsifier. + +Both additions use one schema/source7 change. They must not be shipped as separate incompatible version increments. Preserve source1–6 closed admission, including explicit default/null new fields. Prefer serialization omission of operation=None to preserve existing body bytes, backed by frozen nonempty old5/6 fixtures and exact fingerprint adapters; serde defaults alone are insufficient evidence. Version-token-only source upgrades preserve includes, ownership, comments and raw invalid drafts. Existing transient impact approvals reset; authored review decisions remain raw intent and are replanned. Never restore a serialized executable plan/command. Old5/6 Call/Reply/value/SequenceLink/trace/view-selector checkpoints and combined new7 restore are release gates. + +## Resolved critique boundaries + +- Admission counts authored path components deterministically. Resolver cache capacity only evicts/skips storage; it never changes validity. Per-request analysis budgets are separate. Initial depth64, total ordered-reference budget50000, analysis50000nodes/100000edges remain explicit service/desktop resource policies and must pass scale/cold/warm/eviction tests before acceptance. +- Normal Delete removes the whole dependent ContextualLink atomically when its owner, path member, leaf or purpose work/exchange is deleted. It never shortens/reinterprets the address or changes purpose. Reidentify visits every typed component. Retargeting that invalidates a surviving link refuses unless an explicit valid repair batch is supplied. Caller ownership controls source placement even when both leaves are external. +- Accept the [contextual analysis contract](release-chain-context-analysis-contract-20260906.md): separate precedence, assembly incidence and coverage projections; qualify nested definitions by occurrence paths; mark synthetic reference boundaries and exclude their arcs from authored cardinality/guards. A qualified advisory may flag R1.B's two incoming authored precedence links, without invalidating the model, forcing a Join, changing standalone P/R2 or claiming execution analysis. Missing/empty/over-budget expansion is explicitly incomplete, never a guessed bypass or complete-looking reachability result. Direct-only historical diagnostics remain unchanged. +- A selected operation requires proof through the **actual concrete Standard component ports** of the Call: receiver Provides, sender Requires a compatible interface exposing that same allocation. Participant containment does not inherit interface roles onto ports. Refuse a selected allocation without this proof; explicit unresolved operation intent remains editable. Resolve inherited allocations through the existing data rules; never substitute a separately allocated same item. Call/Asynchronous transmission and actual reversed-channel direction follow the operation contract. +- Reply derives roles and signature from the Call, then separately validates existing reply correlation/order/reversed participants/channel rules. A separate explicit return channel remains permitted when it carries the selected operation item (or an admitted subtype) and its actual endpoints satisfy the existing channel contract; this check applies even to parameterless/void returns. Do not demand that the Reply receiver provide the operation. Ordinary operation=None replies retain their prior behavior. +- Deleting an operation allocation clears its selection to present/unresolved and preserves still-live value expressions. While signature is unresolved, perform independent live-reference/path checks and report incomplete signature; do not fall back to the whole channel's union as a substituted operation. Item/field deletion uses normal proven-reference cleanup. Reselection validates retained values against the exact new signature and rejects without implicit pruning/defaults. +- SA→OA operation abstraction requires explicit per-source omission with rationale in the transition preview. Missing/irrelevant/duplicate/wrong-direction decisions reject; Reply derives the Call's abstraction and has no duplicate writable choice. Preserve the source, map remaining facts explicitly and validate the whole target. Technical operation Take into an OA NotApplicable slot is not ordinary cross-layer reference mapping. + +## Implementation and acceptance boundary + +One core owner coordinates shared model/accessor/validation/reference/source/version/transition/semantic-facet files for both additions. A separate desktop owner consumes shared resolved context for direct canvas editing, forms and visible labels, including readable minimum-size behavior work. A recovery owner consumes the frozen migration contract and tests raw old/new checkpoints. Root integrates reST/OpenAPI/HTTP/example/package changes and actual Windows/Linux engineering tasks. No worker should invent a parallel path/operation resolver. + +First core checkpoint must compile and pass the concrete repeated/nested OA/SA and two-parameterless-operation counterexamples, structural negatives, exact source/JSON/history/reidentify/delete/migration oracles, before downstream UI acceptance. Then verify reachable graphical authoring, strict REST examples/rejection, semantic-review composition and exact native Save/Undo/Redo/reopen. Existing source6 native evidence remains scoped to its tested executable; new-version migration and rendering require their own evidence. + +Revisit these choices if a concrete valid OA/SA model cannot be represented without inventing engineering facts, a lifecycle/migration counterexample loses authored intent, diagnostics depend on cache/render order, or the working UI cannot expose the identities clearly. Do not add channel-free messages, scenario gates, simulation or later-layer concepts merely as speculative extensions of this work. diff --git a/harness/release-staged-checkpoint-review-20260906.md b/harness/release-staged-checkpoint-review-20260906.md new file mode 100644 index 00000000..581a4b36 --- /dev/null +++ b/harness/release-staged-checkpoint-review-20260906.md @@ -0,0 +1,32 @@ +# Bounded staged checkpoint review + +Reviewed the prospective staged checkpoint on 2026-09-06, before the separate +unstaged semantic-review right-padding change. No new material defect or secret +was found within this review's scope. + +The staged snapshot contained 172 files and 27,469 added lines. A scan of staged +additions found zero private-key headers, AWS access-key patterns, GitHub/OpenAI +token patterns or credential-bearing HTTP URLs. Probe tokens encountered during +manual inspection are explicitly synthetic test values. Ignored native recovery +files, local compiler/runtime binaries, task-local server stores and credentials +were not part of the staged file inventory. The scan output is retained at +`.local/staged-secret-scan.json`; absence of these patterns is not a universal +secret-detection guarantee. + +Manual integration review concentrated on recovery capture/restore and detached +Save As, source-entry relocation across Undo/Redo, exact save receipt adoption, +keyed annotation/binding row replacement, port-workflow recovery and Windows +normal-user manifest integration. These checks were assessed alongside earlier +core tests and native/REST evidence; this review did not rerun Cargo or native UI. +No material contradiction was established in those seams. + +The only staged diff-check diagnostic observed was trailing whitespace in the +upstream `scripts/windows-notices/Fira-OFL.txt` license. Its exact hash-pinned text +is intentionally retained. It is not a production formatting defect and should +not be normalized. + +This was a bounded review of consequential integration risks and secrets, not an +independent examination of every fixture/license line or a full release approval. +The later unstaged semantic-review padding change was excluded. The independently +executed timed two-file native-save oracle is recorded separately in +`release-timed-bundle-oracle-20260906.md`. diff --git a/harness/release-teaching-independent-review-20260906.md b/harness/release-teaching-independent-review-20260906.md new file mode 100644 index 00000000..25420e87 --- /dev/null +++ b/harness/release-teaching-independent-review-20260906.md @@ -0,0 +1,34 @@ +# Independent teaching-project review — 2026-09-06 + +Scope: the new `examples/release1` model/builder, generated source/interchange, tutorial/evidence, generator, and `release1_teaching` tests. I did not author those model files. My Home hook is excluded from this independent assessment. No Cargo, native input, model edits or HTTP calls were performed for this review. + +## Findings + +| Priority | Finding and evidence | Smallest useful correction | +| --- | --- | --- | +| P1 workflow | `examples/release1/README.md:5` presents Save As to a new folder as equivalent to copying the lesson, while the evidence target is relative (`sa/behavior/scenario_sa_conversation.arcadia`, artifact URI `evidence/repair-check.md`). Save As enumerates only `snapshot.bundle.files` and calls the source writer (`crates/oa-desktop/src/app.rs:3883`); it does not copy the Markdown evidence or tutorial. A fresh working folder therefore loses the linked evidence after following the suggested route. | Prefer copying the entire lesson folder before opening/editing. If using Save As, explicitly copy `evidence/` too and keep the tutorial available. Do not broaden generic Save As into automatically copying arbitrary artifact targets. Root accepted this finding and requested consistent tutorial/Home/guide wording. | +| P2 semantic scope | `model.rs:545` gives **Repair request and measured reply** the `triage` capability context, named **Assess an alarm** (`model.rs:233`), although its precondition says the alarm has already been assessed (`model.rs:93`) and its two messages request repair and return pressure after repair. Generated OA source confirms `context @oa_triage` at `oa/behavior/scenario_oa_conversation.arcadia:2`; the real transition carries the same mismatch into SA. This is structurally valid, but assigns the demonstrated repair behavior to the wrong stated engineering objective. | Assign the repair conversation to the restoration capability, or deliberately rename/redefine its capability to include repair coordination and reconcile the assessment-support link. Assessment already has an explicit CapabilityInvolvement, so a misleading repair context should not be needed to suppress a support warning. Re-run the one-warning oracle after the model author chooses the correction. No model change was made by this reviewer. | +| P2 release regression gap | `crates/oa-core/tests/release1_teaching.rs:15` compares generated baseline/request between two builds, and lines 18–29 compare shipped source/model JSON. It never compares shipped `transition-before.json`, `transition-request.json` or `expected-diagnostics.json` against that generated result. The tutorial advertises these files as exact replay/diagnostic inputs. They could become stale while all four tests pass. | Extend the existing deterministic/shipped-file test to decode and compare all three advertised JSON inputs. This is an evidence gap, not a demonstrated mismatch in the current files. | +| P3 discoverability | `README.md:47` calls the intentional-warning function **Audit alarm thresholds**, but the actual name is **Audit pressure thresholds** (`model.rs:891` and generated `sa/functions/function_sa_audit_thresholds.arcadia`). The alias is correct, so this does not invalidate the diagnostic. | Match the actual displayed name. | + +## Substantive checks and bounded conclusions + +The main engineering story is coherent: OA participants directly perform leaf activities; Interactions connect those activities and explicit ExchangeAllocations associate both directions with an unoriented CommunicationMean. The chosen SA service boundary and external technician are explicit mappings. The transition request clears field copies so it preserves the deliberately different System name, and supplies both Standard component-port Create decisions. Four later PortAllocations and two ExchangeAllocations implement the stated interface refinement; the tutorial explicitly distinguishes that later refinement from the first transition. + +The process uses explicit Choice/else/Merge SequenceLinks, and its SequenceReference reuses the field procedure by identity. Scenario Call/Reply values are typed InOut pressure parameters; Reply refers to the actual Call and reverses its participants. ScenarioUse has two explicit bindings. The state machine's Change event, guarded return, and Entry StateAction are typed model facts. The narrative explicitly disclaims executable hydraulic/control/safety validation. The one unfinished leaf function has a responsible participant but no behavior use, so the advisory warning and suggested modeling decision are appropriate. Architecture SVG scope is stated without claiming faithful behavior export. + +Source-address guidance for the reviewed name change matches the actual SA module and traceability file. The test performs a real semantic transition and checks unrelated modules remain identical. Deterministic aliases/UUIDs are assigned to generated identities through the shared reidentify mechanism, with graph/reference preservation checked by canonical source round-trip. Full/filtered views share semantic and technical-port geometry; the focused view's empty work-category selection intentionally removes functional traffic. + +A read-only JSON consistency probe found 65 requested transition sources matching exactly all OA baseline elements and non-ExchangeAllocation relations, two explicit participant mappings, one communication endpoint decision, zero copied existing fields, and no differences between baseline/final OA element records. This does not substitute for replaying the shipped request through REST. The documented raw JSON and SourceBundle import shapes match `routes.rs:318`/`:339`; the tutorial accurately says these routes do not fetch local artifact targets and does not claim HTTP/native acceptance of this particular lesson. + +The author reported four focused core tests and strict Clippy passing. This review inspected their code, not a fresh run. Native discoverability, the full 25-minute learner workflow, copied evidence availability, and the exact HTTP lesson replay remain separate acceptance evidence. No broad release-complete claim follows from this review. + +## Follow-up status + +Root accepted the evidence-copy finding. The reviewer corrected README/Home/guide visible instructions to prefer copying the entire lesson folder and to require separately copying `evidence/` when using Save As; generic save behavior is unchanged. README now uses the actual **Audit pressure thresholds** display name. These copy-only edits are formatted/diff-check clean and **not rebuilt** while the Linux VM owns resources; the existing `246B…` walkthrough stage and Linux snapshot precede them. + +The model author completed the scenario correction: both OA and SA repair conversations now use the restoration capability. A separate **Assess an alarm procedure** contains an Action using assessment and explicitly contributes to the assessment capability; the main process references this procedure. Independent inspection of the builder, generated conversation source and assertions confirms this addresses the scope problem without removing the assessment obligation or disguising it with an unrelated scenario. The current bundle has 30 source files; README now states 29 included modules and describes both distinct capability contexts. + +The shipped-input gap is also closed in source: the deterministic test now decodes and compares `transition-before.json`, `transition-request.json` and `expected-diagnostics.json` against the generated result. The author reports four expanded tests passing through direct rustc with warnings denied. This reviewer did not rerun those tests or replay HTTP; the previous native stage and immutable Linux snapshot still predate these teaching corrections. The earlier 65-source read-only probe above describes the pre-correction lesson, not the new request. + +Subsequent packaging update: the corrected visible guidance compiled and passed strict workspace Clippy with the behavior-transition build. Stage `.local/native-qa/bin-source6-behavior-context` (`F5ECAE…` desktop) includes all 37 current teaching files, including 30 source files, each hash-verified against the working folder. This closes the earlier unbuilt-copy status for that stage only; no native walkthrough of it is claimed here. diff --git a/harness/release-teaching-native-20260906.md b/harness/release-teaching-native-20260906.md new file mode 100644 index 00000000..48beed17 --- /dev/null +++ b/harness/release-teaching-native-20260906.md @@ -0,0 +1,31 @@ +# Teaching project native acceptance — 2026-09-06 + +Status: bounded onboarding, reviewed-name/source/history and reopen checks pass; complete tutorial acceptance is still in progress. Root operated the actual Iced application through the authorized Computer Use API. No UI state was synthesized through application internals. + +## Exact environment + +Windows 11, minimum client size 1100×700 (captured window1102×732), process9672, staged `.local/native-qa/bin-source6-behavior-context/arcadia-studio.exe`. GUI SHA256 `F5ECAEC02BBAFBC6F3BDC6ADEB746AFE91F3A495C6D9564A68DEA41E61AC9246`; server `1F98F02DBC9FB2D9AFA3C42BB0CB4038BAAE81C85EF17365B5CB486669E749F8`; runtime DLL `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4`. The corrected packaged example has94elements,62relations,5views,68traces and30reachable source files; the whole folder contains37files. This differs from the earlier28-source Linux snapshot. + +The whole staged example was copied to `.local/native-qa/water-pressure-working`, including its README/evidence/generator inputs. All37file hashes matched the staged original. Tests used the copied `project.arcadia`; packaged teaching sources were preserved. + +## Observed and checked + +- Home displayed complete whole-folder copy guidance and a reachable walkthrough action at minimum size. Home and the end of the guide's Build first model topic both opened the corrected packaged project. Returning to the copied project through recent models worked. +- Source displayed30files, the entry's includes, and syntax highlighting. Appending the invalid raw text `{unfinished 雨` then attempting the Home walkthrough action produced the normal Keep changes guard. Cancel retained the exact raw editor buffer and located lint (`project.arcadia34:13`, invalid token雨). One Undo Text restored the original draft. All37copied files remained byte-identical; the trial made no file save. +- Before the tutorial rename, Model checks showed the one intended warning. Its Open action selected SA `@sa_audit_thresholds` and its actual description/performer context. +- OA `@oa_confirm` was renamed to `Confirm stable pressure`, applied and saved. The complete folder was preserved as `.local/native-qa/water-pressure-before-review`. +- OA→SA semantic review selected the existing counterpart and Header Name, Take Operational: one decision, zero resets and zero additional impacts. Apply changed SA `@sa_confirm` to `Confirm stable pressure`, preserving the deliberately distinct System name. Save all succeeded on its first submission. Relative to the OA-only rename, exactly `sa/functions/function_sa_restore_work.arcadia` and `traceability.arcadia` changed. The accepted37files were preserved as `.local/native-qa/water-pressure-reviewed-accepted`. +- The independent checker `harness/native-release1-review-check.rs` reproduced the lesson generator, OA rename and actual core semantic NameTake/source update. The actual full Project and all30source files matched without UUID, metadata, ordering or whitespace normalization. Separate OA-only and reviewed checks passed. +- One Undo/Save restored all37files exactly to the OA-only baseline. One Redo/Save restored all37files exactly to the accepted review. The root reran the independent `review` checker after Redo: PASS full model/all30source, with preserved OA baseline verified. `.local/native-qa/water-pressure-redo-hashes.json` records37comparisons/zero mismatches. All four saves (OA rename, reviewed result, Undo, Redo) succeeded first submission. +- Home recent-model reopen restored the accepted saved project and the visible OA reviewed name; no dirty indicator. The independent full model check establishes both names and unchanged System identity; further SA visual reopen inspection belongs to the follow-up navigation-fix replay. +- Behavior opened the assessment procedure and the main alarm-response process. At100%, the assessment reference and its1/1source preview were readable and selectable as `@oa_assess_step`. Fit overview showed the full six-contribution topology at23%. Opening Two paths showed a pending form with Create disabled and reachable Cancel; Cancel restored clean state without creating records. No two-path commit or full reusable-procedure drill-down is claimed from this trial. + +## Findings and remaining work + +Semantic Apply produced correct accepted data but changed the layer/selection after view normalization: SA properties appeared over the previous OA canvas, and the diagram picker read Choose diagram. A fresh snapshot reproduced it. Undo subsequently normalized the SA view; this was a navigation defect, not source corruption. `release-semantic-review-navigation-20260906.md` owns the focused fix/tests and its later native replay. This binary predates that fix. + +The process overview at minimum size is too small for label reading;100% exposes less than one complete column and requires both horizontal and vertical scrolling. Selection works, but these observations do not establish an ergonomic complete process lesson. Reference-label double-click and right-click did not navigate in this trial. Revisit the working-space arrangement and explicit reference navigation while implementing the independently established contextual-chain capability. + +Remaining tutorial gates include readable complete process/scenario/reuse work, interface/data inspection, goal/view comparisons, state-action evidence and artifact-target navigation. The deliberate initial diagnostic count is not a claim that a subsequently edited model has the same count. The native NameTake path can create a new semantic-drift notice elsewhere; inspect it rather than treating the status-bar count alone as corruption. + +Process9672 was closed cleanly with Alt+F4 before the next rebuild. Runtime logs are `.local/native-qa/studio-source6-behavior-context.{stdout,stderr}.log`; no recovery or forced stop was part of this tutorial pass. Earlier communication reverse checks on the same binary retain their separate complete source/history evidence. diff --git a/harness/release-timed-bundle-oracle-20260906.md b/harness/release-timed-bundle-oracle-20260906.md index 3be4701c..5aeee1da 100644 --- a/harness/release-timed-bundle-oracle-20260906.md +++ b/harness/release-timed-bundle-oracle-20260906.md @@ -33,7 +33,7 @@ endpoints, empty free-text trigger, guard `pressure_valid`, typed effect model metadata, state actions, containment, diagram placements, sizes, collapse states, routes, traces and artifact links remain equal. -Counts are pristine14→saved15 elements,10 relations,1 diagram and8 placements. +Counts are pristine 14→saved 15 elements, 10 relations, 1 diagram and 8 placements. The saved graph has no structural errors. Its three existing `work_without_behavior_context` warnings address the three work functions; typed state actions do not constitute process/scenario evidence under the coverage policy. diff --git a/harness/release-view-duplication-dense-plan-20260906.md b/harness/release-view-duplication-dense-plan-20260906.md new file mode 100644 index 00000000..95d600dc --- /dev/null +++ b/harness/release-view-duplication-dense-plan-20260906.md @@ -0,0 +1,132 @@ +# Saved view duplication and dense exchange evaluation + +2026-09-06. Design against current integrated source. Root subsequently authorized +the bounded pure builder implementation noted below. No native operation or +performance/readability pass is claimed here. + +## Duplicate a saved presentation + +Use one existing `Command::CreateDiagram(Diagram)` transaction. It already accepts +embedded placements and runs ordinary global identity, alias, diagram and reference +validation (`oa-core/src/session.rs`). A new command variant or schema is unnecessary. + +The pure builder takes an accepted Project plus source Diagram identity and returns +a fresh Diagram. Copy name with a readable “copy” suffix, description, layer, +projection (including the same StateMachine identity), ViewOptions and routes. +Allocate a fresh diagram identity/alias and a fresh identity/alias for every +NodePlacement. Preserve each placement's semantic element, position, size, +collapsed flag and port anchors. Route edge identities, waypoints, label fractions +and offsets remain unchanged; they reference semantic edges, not old placements. +Visual parents are derived by projection, so no parent-placement remapping is needed. + +Reserve newly allocated identities and aliases in local sets alongside the accepted +Project's identities. Calling `fresh_alias` repeatedly against an unchanged Project +does not reserve aliases generated for other new records. Do not run global +`reidentify`, which changes accepted semantic references. Do not clone existing +artifact links owned by the old diagram or its placements: those links make claims +about those particular artifacts; duplication of presentation does not establish +new claims. Semantic links remain available through the shared semantic elements. + +Implementation now prepared in the new `oa-desktop/src/view_copy.rs`: +`command(&Project, source: Id) -> Result`. It clones only the +Diagram, reserves fresh identities and readable aliases locally, and returns the +existing CreateDiagram transaction. Three focused tests cover manual typed port +anchors/routes/filters/artifact ownership with exact source/Undo/Redo oracles, +repeated nested collapsed state-view duplication and global alias uniqueness, and +missing-source rejection. Formatted and handed to root for module/action wiring; +compilation and execution are pending, not claimed by this report. + +Update after `.local/source6-desktop-tests.log`: all three pure duplication tests +and both selective property tests passed. Root wired DuplicateDiagram actions in +architecture and state context menus to their actual diagram identities. Independent +app review found that checking a missing source only after `apply()` could commit +an unrelated pending property edit; root added a preflight before Apply. Five new +`app/view_copy_tests.rs` regressions now cover actual state/architecture view routing, +one source Undo/Redo, stale request preserving a valid raw draft, invalid properties, +pending behavior and incomplete source. They await root's next test run. The same +review found no omitted invalidation among accepted Document replacements, Undo, +Redo and Save As source relocation; cache and SVG tests passed in the above log. + +The new `oa-core/examples/release_dense_views.rs` generates canonical source/JSON +and an oracle report in a supplied directory. It has fixed identities, eleven +visibility cases, actual same-pair projection assertions, immutable placement/model +checks and source/JSON reopening assertions. Routing timing, overlapping label pairs +and identical path pairs are observations rather than a usability pass. Root owns +execution and native inspection; no generated output is claimed before that run. + +Desktop seam: add an action next to the saved-view chooser and reuse `App::apply` +before the builder/transaction. This preserves existing rejection behavior for +source/behavior drafts and invalid property values. A pending valid property Apply +can be a preceding ordinary edit, as with existing `AddDiagram`; do not promise +that these two separate actions undo together. After success, select the duplicate, +clear obsolete placement/bend selections and use the existing diagram activation +path, including routing StateMachine views to Behavior. On Behavior use the actual +`behavior.diagram.diagram`, not a potentially stale architecture `active_diagram`. +No duplicate action should invent a default view when its source no longer exists. + +| Acceptance case | Exact oracle | +| --- | --- | +| Ordinary nested allocation view | Same projected semantic nodes, nesting, ports and geometry after replacing only placement identities; original diagram unchanged. | +| StateMachine view | Same machine, regions, collapse, typed action/internal rows and transition routes; active workspace uses existing state tools. | +| Traceability view | OA and SA placements and reference choices preserved, including explicit cross-layer hides. | +| Saved filters and routes | All vs empty Only/Any preserved byte-for-byte in typed values; hidden or currently folded route retained. | +| Name/alias collision | Duplicate twice with deliberately occupied copy aliases; each new record globally unique and source parse succeeds. | +| Transaction/Undo | Exactly one additional Diagram with remapped placement identities; elements, relations, traces and ArtifactLinks equal original. One Undo removes copy; Redo restores the same new identities. | +| Source bundle/REST | Existing generic command endpoint and source serialization can express the identical CreateDiagram; save/reopen actual included bundle equals accepted Project. Do not assume the original view's source file is reused. | +| Invalid/pending drafts | Clone rejected or handled by existing explicit Apply semantics; raw source, property, behavior and workflow drafts retained. No hidden discard or partial diagram. | +| Missing source/identity collision | Builder/command returns an error and preserves model, source and undo/redo stacks. | + +## Dense forty-exchange fixture + +Build a deterministic source-6 fixture from production records: two OA Activities +(`Request service`, `Provide service`), two Entities allocated to them, forty +Interactions `Service data 01` through `Service data 40` with the **same ordered +activity endpoints**, and four ExchangeItems (one per ten exchanges). Use fixed +UUIDs in a reserved fixture prefix and explicit aliases, with no user data. + +Four Work categories contain exchanges 01–10, 11–20, 21–30 and 31–40 respectively. +A fifth overlapping category contains 01, 11, 21 and 31. Three Processes explicitly +list exchange occurrences for the first three groups; 31–40 are deliberately outside +behavior. One FunctionalDataflow view has exactly two placed Activity nodes at +(80, 160) and (760, 160), ordinary node sizes and no authored routes or label offsets. +The initial representation therefore has exactly forty graphical edges between the +same pair. Do not make the test pass by spreading the semantic endpoints into fake +nodes or pre-authoring forty hand-tuned routes. + +The fixture can additionally contain an allocation view of the same model and a +duplicate of the dataflow view for the above duplication tests, without changing +the forty-edge baseline. Participant means/categories should be exercised in their +own view so the dense work-exchange oracle remains unambiguous. + +| Settings / operation | Deterministic semantic or projection oracle | +| --- | --- | +| Defaults | 40 actual exchange IDs; no semantic reduction, route metadata invented or model edit. | +| First category only | Exactly 01–10; no node, port, placement, collapse or route mutation. | +| First + overlapping categories | Exactly 01–10 plus 11, 21 and 31 = 13 IDs. | +| In behavior / outside behavior | Exactly 01–30 / 31–40. | +| Second process context | Exactly 11–20. Outside behavior + second process = 0. | +| Empty category/context or Only([]) | 0 work lines; source distinguishes this from All. | +| Hide 05, then change label/category | Hide remains; first category displays 9. Clearing hide displays 10. | +| Name / Items / Names and items | Exact declared text with group payload reuse; visibility and model facts unchanged. | +| Save, restart, duplicate | Typed saved options and untouched geometry match; duplicate identities differ only for diagram/placements. | + +Native evaluator (root sole operator): at a normal 1280×800 window, open the +unmodified dense view, Fit, and inspect at fit zoom and 100%. Record elapsed time +and screenshot; select named exchanges 01, 10, 20 and 40, verify Properties shows +their exact identity, then filter to the first concern and restore All. Repeat +at the supported minimum window size. Capture whether names are legible, labels +overlap nodes/each other, fit makes all labels microscopic, or coincident route +hits select only the same first edge. Current `diagram::hit_edge` breaks equal +distance by selected identity then UUID, so coincident routes cannot be assumed +individually discoverable. Existing automatic routing avoids previous segments and +settles labels afterward, but that is not evidence forty routes remain usable. + +Pass requires no lockup, no unreadable overlap among the displayed labels at the +task's working zoom, and a discoverable way to inspect any of the forty actual +exchanges without manually rerouting them. Use timing as measured evidence; a new +arbitrary timing cutoff must not replace observation on the two-CPU test machine. +If baseline display fails, representation-only grouping/summary becomes required: +one visible aggregate must explicitly state its count, expose all member identities +and allow member inspection/unbundle without creating a new relation or rewriting +categories. Filtering to ten exchanges alone does not prove the unfiltered dense +case is solved. Agree the exact summary display contract before adding source fields. diff --git a/harness/release-walkthrough-hook-20260906.md b/harness/release-walkthrough-hook-20260906.md new file mode 100644 index 00000000..b9f90e0c --- /dev/null +++ b/harness/release-walkthrough-hook-20260906.md @@ -0,0 +1,27 @@ +# Teaching walkthrough Home/guide hook — 2026-09-06 + +Home now offers **Open water-pressure walkthrough**, also available in the guide's First Project article. It opens `examples/release1/project.arcadia`, titled “Restore safe water pressure — release 1 walkthrough,” through the existing guarded pending action and source-bundle loader. The UI directs users to `examples/release1/README.md` and to use Save as before editing their working copy. This is an ordinary opened local bundle, not an embedded/generated substitute or a detached unsaved demo. + +Resolution checks `examples/release1/project.arcadia` beside the running executable first, independently of the working directory. Debug builds may fall back to the checkout. Release builds require packaged assets. Missing assets produce a specific status explaining that the complete folder is needed. Existing dirty model/source/behavior guards precede opening; malformed/missing included files remain subject to the existing loader checks. + +## Verification + +- `cargo test --workspace --locked walkthrough`: three passed, 527 desktop tests filtered, 0.10 s. Two new tests cover packaged-file precedence/debug fallback/missing-assets reporting and the ordinary unfinished-source pending guard with cancellation and unchanged accepted model. The third is the existing guide search test. Log: `.local/walkthrough-hook-tests2.log`. +- The first run had two passes and one test-only failure comparing mixed Windows slash spellings in the diagnostic path. The assertion now normalizes separators; resolved PathBuf comparisons already passed. Original log: `.local/walkthrough-hook-tests.log`. +- Strict `cargo clippy --workspace --all-targets --locked -- -D warnings` passed in 14.17 s. Log: `.local/walkthrough-hook-clippy.log`. +- `cargo build --workspace --bins --locked` passed in 29.79 s. Log: `.local/walkthrough-hook-build.log`. +- All commands used one Windows Cargo job. This focused follow-up did not repeat the previous complete 527-test desktop suite; compiled desktop inventory is now 530. + +## Staging and release gate + +Staged, not launched, `.local/native-qa/bin-source6-walkthrough` contains GUI, server, `libunwind.dll`, and the entire `examples/release1` folder. All **35 example files** (including the 28 source-bundle files) were copied and individually hash-compared against their source. Evidence: `.local/walkthrough-staged-files.json`. + +SHA-256 (`.local/walkthrough-staged-sha256.txt`): + +- GUI: `246BACE21E254985EE90EF05132139DD7D6AF75085DE9E58FEB49477A493407D` +- Server: `1F98F02DBC9FB2D9AFA3C42BB0CB4038BAAE81C85EF17365B5CB486669E749F8` +- `libunwind.dll`: `F45604A12E8BCD839FD86F667C1BE1CC7C4F9DA2B3C69FF381AC708F0E1AE7D4` + +The Windows package script includes examples through `git ls-files`; at handoff the new folder had zero tracked entries. **Stage the complete example folder before release packaging**, or it will be omitted. No packaging script behavior was changed. Native Home/guide hook navigation, bundle opening from the staged application, and Save as are pending root verification; this report does not claim those UI checks passed. + +Subsequent independent lesson review corrected the visible-copy guidance in README/Home/guide: copy the entire lesson folder before editing, or separately copy `evidence/` when using Save As because it persists only model source. The staged `246B…` binary and copied README above precede this correction; those copy-only edits await the next root build/stage. See `release-teaching-independent-review-20260906.md` for findings and scope. diff --git a/harness/release-walkthrough-hook-independent-review-20260906.md b/harness/release-walkthrough-hook-independent-review-20260906.md new file mode 100644 index 00000000..0621338c --- /dev/null +++ b/harness/release-walkthrough-hook-independent-review-20260906.md @@ -0,0 +1,26 @@ +# Walkthrough hook independent review — 2026-09-06 + +Read-only review of the Home/guide walkthrough entry point and its existing open/save integration. No Cargo, native input, or production edits were performed. The hook is in `home.rs`, `home/tests.rs`, `app/checkpoints.rs`, `app.rs`, and `guide.rs`; persistence is the existing `app.rs`/core source-files path, with no separate new storage or workflow adapter. + +## Finding and correction + +**P2, corrected in source; rebuild/native verification pending:** the original Home/guide/lesson instruction to use Save As for a working copy omitted external lesson assets. `app.rs::write_snapshot_inner` persists `CompiledSourceProject.bundle.files`, not arbitrary sibling files. The lesson's SA scenario references `evidence/repair-check.md`, and its project description points to `README.md`. Opening the packaged lesson and saving its model into an empty folder therefore preserves that URI but does not copy its target or the tutorial. This can produce a valid model with an incomplete local lesson folder. + +Root accepted this independently identified finding, also reported by the teaching reviewer. The bounded correction now visible in Home, guide, and `examples/release1/README.md` instructs copying the **whole lesson folder** before editing. It explicitly says Save As copies model source only and requires separately copying `evidence/`. That addresses the misleading instruction without changing general Save As semantics. See [the teaching review](release-teaching-independent-review-20260906.md). + +The hook deliberately opens a normal, path-bound local project. Opening it does not write files; subsequent ordinary Save can write to the original lesson location. This is an explicit design boundary, not an independent overwrite defect. The complete-folder-copy instruction is material to preserving the distributed original. + +## Guard and loading assessment + +- Home and guide use the same `Home::Walkthrough` event, forwarded to `Request(Pending::Walkthrough)`. The existing dirty predicate includes accepted changes, raw property/source drafts, pending behavior, and workflows. Normal modal and busy guards also apply. +- Cancel leaves the current model and unfinished source untouched. Discard authorizes proceeding but does not clear them before loading. Resolver failure or an I/O `Loaded(Err)` only changes status. The Save branch retains the existing failed/cancelled-save handling and only proceeds after a successful source save; JSON export stops continuation. Save receipts retain document identity and source-entry adoption checks. +- `walkthrough_path` resolves relative to the executable, independently of the working directory. Packaged assets take precedence; only debug builds have the checkout fallback. A missing entry produces a specific complete-folder restoration message. Entry existence is merely resolution; the ordinary source loader then reads the real modular bundle. +- Successful current-schema loading binds the actual entry path and installs its compiled source workspace. Source diagnostics take the ordinary repair path, retaining the last accepted graphical model while opening the loaded source drafts. This replacement follows the same dirty-work decision as other opens; no new unapproved draft-loss path was established by this review. + +No further material correctness finding was identified in these bounded paths. This is code inspection, not an independent execution of Save/Discard/failure paths or a native acceptance claim. + +## Evidence and remaining checks + +The hook report records three focused walkthrough tests passing, strict all-target Clippy passing, and GUI/server build passing: [hook evidence](release-walkthrough-hook-20260906.md). The resolver test meaningfully checks package precedence, debug fallback, and missing-entry diagnostics. The new app test checks unfinished raw source, accepted-model identity, pending guard, and Cancel. These tests do **not** exercise the full 28-file lesson, external asset copying, or hook-specific Save/Discard/error completion. The existing storage tests supply broader coverage, but were not rerun here. + +The staged `246BACE21E254985EE90EF05132139DD7D6AF75085DE9E58FEB49477A493407D` GUI and 35 hash-checked lesson files precede the copy-instruction correction. Root native verification of the staged hook and a subsequent corrected build remains separate evidence. Release packaging uses `git ls-files`; the complete newly added lesson folder must be staged before packaging, as already recorded in the hook report. This review does not claim a final portable package or a complete 530-test desktop rerun.