use oa_core::{language, persistence, validation, *}; fn read(source: &str) -> Project { language::parse(source).unwrap_or_else(|e| panic!("{e:?}\n{source}")) } fn rejected(source: &str, fragment: &str) { let errors = language::parse(source).expect_err(source); assert!( errors.iter().any(|e| e.message.contains(fragment)), "Expected {fragment:?}: {errors:?}" ); for e in errors { assert!(e.line > 0 && e.column > 0); assert!(source.is_char_boundary(e.span.start)); assert!(source.is_char_boundary(e.span.end)); } } fn complete_model() -> Project { let mut p = read( r##" arcadia 2; project mission "Harbor safety — 港 🌊" { description "Need and selected system response.\nVerifiable in both editors."; oa_entity coordination "Coordination" {} oa_entity team "Response team" { parent @coordination; } oa_actor operator_one "Operator" { parent @team; } oa_role response_role "Response role" { parent @team; } oa_activity operational_work "Operational work" {} oa_activity report "Report event" { parent @operational_work; } oa_activity assess "Assess event" { parent @operational_work; } oa_capability safe_harbor "Safe harbor" {} oa_capability routine_safety "Routine safety" {} oa_capability emergency_safety "Emergency safety" {} oa_mission operational_goal "Protect harbor users" {} mission_capability operational_mission_support "Required capability" { source @operational_goal; target @safe_harbor; } mission_involvement operational_mission_participant "Mission participant" { source @coordination; target @operational_goal; } capability_include required_safety "Includes safety" { source @safe_harbor; target @routine_safety; } capability_extend emergency_extension "Emergency extension" { source @emergency_safety; target @safe_harbor; condition "sea_state > 4 && operator == \"Ready\""; } capability_generalization specialized_safety "Specializes safety" { source @emergency_safety; target @routine_safety; } oa_process response_path "Response path" { steps [@first_exchange, @repeat_exchange]; } oa_process reviewed_path "Reviewed response" { steps [@review_start, @review_response]; } oa_occurrence review_start "Assess before response" { parent @reviewed_path; body { Action { action @assess; lifeline none; }; }; } oa_occurrence review_response "Reuse response path" { parent @reviewed_path; body { SequenceReference { sequence @response_path; }; }; } sequence_link reviewed_follow "Continue response" { source @review_start; target @review_response; guard "assessment == \"Approved\""; } allocation reporting "Performs" { source @operator_one; target @report; } allocation assessment "Performs" { source @coordination; target @assess; } role_realization responsible "Realizes role" { source @coordination; target @response_role; } role_assignment assigned_work "Assigned work" { source @response_role; target @assess; } interaction event_report "Event report" { source @report; target @assess; items [@operational_information]; } capability_involvement support_work "Supports" { source @assess; target @safe_harbor; } chain_involvement support_path "Illustrates" { source @response_path; target @safe_harbor; } oa_occurrence first_exchange "First report" { parent @response_path; body { Exchange { exchange @event_report; sender none; receiver none; }; }; } oa_occurrence repeat_exchange "Repeated report" { parent @response_path; body { Exchange { exchange @event_report; sender none; receiver none; }; }; } oa_exchange_item operational_information "Operational report" { kind Information; } oa_event incoming_report "Report arrived" { body { Signal { items [@operational_information]; }; }; } oa_data_type operational_record "Operational record" { definition Record; } oa_data_type weather_state "Weather classification" { definition { Enumeration { literals ["Clear", "Fog", "Storm"]; }; }; } oa_data_field weather_field "Weather" { parent @operational_record; type_ref { Named @weather_state; }; minimum 0; maximum none; unit ""; } oa_interface operational_contract "Operational communication" { items [@operational_information]; } oa_port operational_port "Report channel" { parent @coordination; direction InOut; interface @operational_contract; } oa_scenario operational_story "Operational story" { context @safe_harbor; steps []; } oa_state_machine operational_modes "Operational modes" { parent @coordination; } oa_region operational_region "Operational region" { parent @operational_modes; } oa_initial operational_initial "Start" { parent @operational_region; } oa_mode operational_normal "Normal" { parent @operational_region; entry "Begin watch"; exit "End watch"; do_activity "Coordinate"; } state_transition operational_start "Start watch" { source @operational_initial; target @operational_normal; } availability operational_available "Available" { source @assess; target @operational_normal; } state_action operational_do "Perform assessment" { source @operational_normal; target @assess; phase Do; } oa_constraint operational_limit "Visibility limit" { expression "visibility >= threshold"; constrained [@assess, @event_report]; } sa_system safety_system "Harbor safety system" {} sa_actor duty_officer "Duty officer" {} sa_function system_service "Provide event support" {} sa_function detect "Detect event" { parent @system_service; } sa_function notify_officer "Notify operator" { parent @system_service; } sa_capability supported_response "Supported response" {} sa_mission mission_goal "Protect harbor operations" {} mission_involvement system_mission_participant "System mission participant" { source @duty_officer; target @mission_goal; } sa_functional_chain selected_chain "Detection to notification" { steps [@system_exchange_occurrence]; } allocation system_detection "Performs" { source @safety_system; target @detect; } allocation operator_notice "Performs" { source @duty_officer; target @notify_officer; } sa_exchange_item system_information "Event information" { kind Event; } sa_interface notification_interface "Notification contract" { items [@system_information]; } sa_port detect_out "Detected event" { parent @detect; direction Out; } sa_port notice_in "Event input" { parent @notify_officer; direction In; } sa_port system_out "System output" { parent @safety_system; direction Out; interface @notification_interface; } sa_port officer_in "Operator input" { parent @duty_officer; direction In; interface @notification_interface; } functional_exchange detected_event "Detected event" { source @detect; target @notify_officer; source_port @detect_out; target_port @notice_in; items [@system_information]; } boundary_exchange boundary_notice "Boundary notice" { source @system_out; target @officer_in; items [@system_information]; } exchange_allocation carried_notice "Carried exchange" { source @boundary_notice; target @detected_event; } interface_provision provides_notice "Provides" { source @safety_system; target @notification_interface; } interface_requirement requires_notice "Requires" { source @duty_officer; target @notification_interface; } mission_capability mission_support "Uses capability" { source @mission_goal; target @supported_response; } capability_involvement system_support "Supports" { source @detect; target @supported_response; } chain_involvement chain_support "Illustrates" { source @selected_chain; target @supported_response; } sa_occurrence system_exchange_occurrence "Event occurrence" { parent @selected_chain; body { Exchange { exchange @detected_event; sender none; receiver none; }; }; } sa_data_type distance "Distance" { definition Real; } sa_data_type event_record "Event record" { definition Record; } sa_data_field named_field "Measured range" { parent @event_record; type_ref { Named @distance; }; minimum 0; maximum none; unit "m"; } sa_state_machine system_modes "System modes" { parent @safety_system; } sa_region system_region "System region" { parent @system_modes; } sa_initial initial_mode "Initial" { parent @system_region; } sa_mode normal_mode "Normal" { parent @system_region; entry "Prepare"; exit "Release"; do_activity "Monitor"; } sa_state degraded_state "Degraded" { parent @system_region; } sa_final final_mode "Stopped" { parent @system_region; } state_transition initialize "Initialize" { source @initial_mode; target @normal_mode; } state_transition degrade "Degrade" { source @normal_mode; target @degraded_state; trigger "sensor failure"; guard "redundancy exhausted"; effect "Annunciate degraded service"; } state_transition stop_system "Stop" { source @degraded_state; target @final_mode; trigger "shutdown"; } availability detection_available "Available" { source @detect; target @normal_mode; } state_action prepare_detection "Entry work" { source @normal_mode; target @detect; phase Entry; } state_action release_detection "Exit work" { source @normal_mode; target @detect; phase Exit; } sa_constraint latency_limit "Latency" { expression "notification latency <= 2 s"; constrained [@detected_event]; } sa_scenario system_story "System story" { context @supported_response; precondition "Watch is active"; postcondition "Operator informed"; steps [@story_exchange, @story_action, @story_state, @alternative_flow]; } sa_lifeline detector_line "Detector" { parent @system_story; represented @detect; } sa_lifeline receiver_line "Receiver" { parent @system_story; represented @notify_officer; } sa_lifeline system_line "System" { parent @system_story; represented @safety_system; } sa_occurrence story_exchange "Send report" { parent @system_story; body { Exchange { exchange @detected_event; sender @detector_line; receiver @receiver_line; }; }; precondition "Event accepted"; postcondition "Report sent"; } sa_occurrence story_action "Detect" { parent @system_story; body { Action { action @detect; lifeline @detector_line; }; }; } sa_occurrence story_state "Normal mode" { parent @system_story; body { State { state @normal_mode; lifeline @system_line; }; }; } sa_occurrence alternative_flow "Alternative" { parent @system_story; body { Fragment { operator Alternative; branches [@good_branch, @bad_branch]; }; }; } sa_occurrence good_branch "Good branch" { parent @alternative_flow; body { Branch { guard "valid event"; occurrences [@branch_action]; }; }; } sa_occurrence bad_branch "Bad branch" { parent @alternative_flow; body { Branch { guard "invalid event"; occurrences []; }; }; } sa_occurrence branch_action "Acknowledge" { parent @good_branch; body { Action { action @notify_officer; lifeline @receiver_line; }; }; } diagram operational_view "OA view" { layer Oa; projection Allocation; } diagram system_view "SA view" { layer Sa; projection Decomposition; } diagram trace_view "Trace view" { layer Oa; projection Traceability; } node report_view { diagram @operational_view; element @report; position { x -0.0; y 12.375; }; size { width 260.0; height 94.0; }; collapsed true; } node detect_view { diagram @system_view; element @detect; } node trace_oa_view { diagram @trace_view; element @report; } node trace_sa_view { diagram @trace_view; element @detect; } trace work_trace "Operational realization" { oa @report; sa @detect; kind Refinement; baseline { oa { name "Report event"; description "Operational intent"; }; sa { name "Detect event"; description "Selected service"; }; }; } trace participant_trace "Participant realization" { oa @coordination; sa @safety_system; kind Participant; } trace goal_trace "Capability realization" { oa @safe_harbor; sa @supported_response; kind Capability; } trace mission_trace "Mission realization" { oa @operational_goal; sa @mission_goal; kind Mission; } trace exchange_trace "Exchange realization" { oa @event_report; sa @detected_event; kind Exchange; } trace path_trace "Path realization" { oa @response_path; sa @selected_chain; kind Sequence; } trace information_trace "Information realization" { oa @operational_information; sa @system_information; kind Item; } trace scenario_trace "Scenario realization" { oa @operational_story; sa @system_story; kind Scenario; } trace mode_trace "Mode realization" { oa @operational_normal; sa @normal_mode; kind State; } trace manual_trace "Engineer rationale" { oa @assess; sa @mission_goal; kind Manual; } artifact design_record "Design decision" { owner @work_trace; relation DerivedFrom; uri "docs/decision%20record.md"; revision "r42"; fragment "#operational-need"; } } "##, ); let state_region = p .elements .values() .find(|e| e.alias == "system_region") .unwrap() .id; for kind in [ Kind::Choice, Kind::Junction, Kind::Fork, Kind::Join, Kind::ShallowHistory, Kind::DeepHistory, Kind::EntryPoint, Kind::ExitPoint, Kind::Terminate, ] { p.add_element(kind, Layer::Sa, kind.label(), Some(state_region)) .unwrap(); } let record = p .elements .values() .find(|e| e.alias == "event_record") .unwrap() .id; for (index, type_ref) in [ TypeRef::Boolean, TypeRef::Integer, TypeRef::Real, TypeRef::String, ] .into_iter() .enumerate() { let id = p .add_element( Kind::DataField, Layer::Sa, &format!("Field {index}"), Some(record), ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataField { type_ref, minimum: 1, maximum: Some(5), unit: "unit".into(), }; } for definition in [ DataTypeDefinition::Boolean, DataTypeDefinition::Integer, DataTypeDefinition::String, ] { let id = p .add_element(Kind::DataType, Layer::Sa, "Primitive type", None) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition }; } for kind in [ ExchangeItemKind::Material, ExchangeItemKind::Energy, ExchangeItemKind::Information, ] { let id = p .add_element( Kind::ExchangeItem, Layer::Sa, "Physical exchange item", None, ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem { kind }; } let scenario = p .elements .values() .find(|e| e.alias == "system_story") .unwrap() .id; for operator in [ FragmentOperator::Parallel, FragmentOperator::Loop, FragmentOperator::Optional, ] { let id = p .add_element( Kind::Occurrence, Layer::Sa, "Control fragment", Some(scenario), ) .unwrap(); *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Fragment { operator, branches: vec![], }, precondition: String::new(), postcondition: String::new(), }; p.elements .get_mut(&scenario) .unwrap() .steps_mut() .unwrap() .push(id); } let owner = p .elements .values() .find(|e| e.alias == "safety_system") .unwrap() .id; let function = p .elements .values() .find(|e| e.alias == "detect") .unwrap() .id; let component_port = Id::new_v4(); let function_port = Id::new_v4(); for (id, alias, parent, contract) in [ ( component_port, "typed_component_port", owner, PortContract::Component { kind: ComponentPortKind::Standard, orientation: ComponentPortOrientation::Unset, }, ), ( function_port, "typed_function_output", function, PortContract::FunctionOutput { outgoing_items: vec![], }, ), ] { p.elements.insert( id, Element { id, alias: alias.into(), name: alias.into(), description: String::new(), parent: Some(parent), body: ElementBody::shared_in( Layer::Sa, SharedBody::Port { contract, legacy: None, }, ), }, ); } p.relate( RelationKind::PortAllocation, "Typed port allocation", component_port, function_port, ); p } #[test] fn all_typed_fields_round_trip_with_stable_aliases_and_occurrences() { let p = complete_model(); let text = language::format(&p); let parsed = read(&text); assert_eq!(parsed, p); assert_eq!(language::format(&parsed), text); assert!(!text.contains("schema_version")); assert!(text.contains("parent @operational_work;")); assert!(text.contains("Named @distance;")); assert!(text.contains("node report_view id \"")); assert!(text.contains("body {\n")); for kind in Kind::ALL { assert!( p.elements.values().any(|element| element.kind() == kind), "missing {kind}" ); } for kind in RelationKind::ALL { assert!( p.relations.values().any(|r| r.kind() == kind), "missing {kind}" ); } let old_aliases = p .identities() .into_iter() .map(|(id, a)| (id, a.to_owned())) .collect::>(); let mut renamed = p.clone(); for e in renamed.elements.values_mut() { e.name.push_str(" renamed"); } assert_eq!( renamed .identities() .into_iter() .map(|(id, a)| (id, a.to_owned())) .collect::>(), old_aliases ); assert_eq!(read(&language::format(&renamed)), renamed); } #[test] fn branching_reused_processes_and_chains_have_lossless_readable_source() { use std::fmt::Write; for (prefix, work, sequence) in [ ("oa", "activity", "process"), ("sa", "function", "functional_chain"), ] { let mut source = format!( "arcadia 2; project topology \"Contextual topology\" {{\n{prefix}_{work} prepare \"Prepare\" {{}}\n{prefix}_{work} assess \"Assess\" {{}}\n" ); writeln!( source, "{prefix}_{sequence} reusable \"Reusable assessment\" {{ steps [@nested_work]; }}" ) .unwrap(); writeln!(source, "{prefix}_occurrence nested_work \"Assessment contribution\" {{ parent @reusable; body {{ Action {{ action @assess; lifeline none; }}; }}; }}").unwrap(); writeln!(source, "{prefix}_{sequence} flow \"Reviewed process\" {{ steps [@entry, @forked, @left, @right, @joined, @choice, @merged, @looped, @reused, @finish]; }}").unwrap(); for (alias, action) in [ ("entry", "prepare"), ("left", "prepare"), ("right", "assess"), ("finish", "assess"), ] { writeln!(source, "{prefix}_occurrence {alias} \"{alias}\" {{ parent @flow; body {{ Action {{ action @{action}; lifeline none; }}; }}; }}").unwrap(); } for (alias, kind) in [ ("forked", "Fork"), ("joined", "Join"), ("choice", "Choice"), ("merged", "Merge"), ("looped", "Loop"), ] { writeln!(source, "{prefix}_occurrence {alias} \"{kind}\" {{ parent @flow; body {{ Control {{ kind {kind}; }}; }}; }}").unwrap(); } writeln!(source, "{prefix}_occurrence reused \"Reuse assessment\" {{ parent @flow; body {{ SequenceReference {{ sequence @reusable; }}; }}; }}").unwrap(); for (index, (from, to, guard)) in [ ("entry", "forked", ""), ("forked", "left", ""), ("forked", "right", ""), ("left", "joined", ""), ("right", "joined", ""), ("joined", "choice", ""), ("choice", "looped", "sea_state > 4 && operator == \"Ready\""), ("choice", "merged", "else"), ("looped", "merged", "assessment accepted"), ("looped", "reused", "repeat assessment"), ("reused", "looped", ""), ("merged", "finish", ""), ] .into_iter() .enumerate() { writeln!(source, "sequence_link path_{index} \"Path {index}\" {{ source @{from}; target @{to}; guard {}; }}", serde_json::to_string(guard).unwrap()).unwrap(); } source.push('}'); let model = read(&source); let canonical = language::format(&model); assert!(canonical.contains("sequence @reusable;")); assert!(canonical.contains("kind Loop;")); assert!(canonical.contains("operator == \\\"Ready\\\"")); assert_eq!(read(&canonical), model); assert_eq!( persistence::decode(&serde_json::to_vec(&model).unwrap()).unwrap(), model ); let mut broken = canonical.replace("sequence @reusable;", "sequence @flow;"); assert!( language::parse(&broken) .unwrap_err() .iter() .any(|d| d.code == "recursive_sequence_reference") ); broken = canonical.replace("sequence @reusable;", "sequence \"reusable\";"); assert!( language::parse(&broken).is_err(), "identity references must use @alias" ); } } #[test] fn authoring_defaults_and_forward_references_remain_strict() { let p = read( r#"arcadia 2; project mission "New" { allocation assigned "Performs" { source @crew; target @work; } oa_activity work "Work" {} oa_entity crew "Crew" {} }"#, ); assert_eq!(p.elements.len(), 2); assert!( p.elements .values() .all(|e| e.description.is_empty() && e.parent.is_none()) ); assert_eq!(p, read(&language::format(&p))); let warnings = validation::validate(&read( "arcadia 2; project mission \"Draft\" { sa_function unfinished \"Unallocated\" {} }", )); assert!( warnings .iter() .any(|i| i.severity == validation::Severity::Warning) ); } #[test] fn typed_unknown_fields_enums_duplicates_and_reference_spelling_are_rejected() { for (source, expected) in [ ("arcadia 4; project mission \"x\" {}", "Unsupported schema"), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { nonsense 1; } }", "Unknown field", ), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { description \"A\"; description \"B\"; } }", "Duplicate field", ), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { description unquoted; } }", "quoted string", ), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { description @mission; } }", "does not accept an @alias", ), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { parent @missing; } }", "Unknown reference", ), ( "arcadia 2; project mission \"x\" { oa_activity a \"A\" { parent \"00000000-0000-0000-0000-000000000000\"; } }", "requires an @alias", ), ( "arcadia 2; project mission \"x\" { sa_exchange_item item_x \"Item\" { kind Unicorn; } }", "unknown variant", ), ( "arcadia 2; project mission \"x\" { sa_exchange_item item_x \"Item\" { kind \"Information\"; } }", "unquoted enum", ), ( "arcadia 2; project mission \"x\" { sa_data_type t \"Type\" { definition { Enumeration { literals [\"One\"]; surprise true; }; }; } }", "unknown field", ), ( "arcadia 2; project mission \"x\" { oa_actor shared \"A\" {} oa_activity shared \"B\" {} }", "Duplicate alias", ), ( "arcadia 2; project mission \"x\" { oa_actor a \"A\" {} allocation x \"X\" { source @a; } }", "Missing required field", ), ] { rejected(source, expected); } } #[test] fn deeply_nested_invalid_draft_returns_a_diagnostic_without_exhausting_the_stack() { let source = format!( "arcadia 2; project mission \"x\" {{ oa_activity a \"A\" {{ parent {}none{}; }} }}", "[".repeat(5000), "]".repeat(5000) ); rejected(&source, "nest more than 64 levels"); let analysis = language::analyze(&source); assert_eq!(analysis.declarations.len(), 2); assert!(analysis.diagnostics[0].span.start < source.len()); } #[test] fn global_identity_and_alias_collisions_include_diagram_nodes_and_project() { rejected( r#"arcadia 2; project mission "x" id "00000000-0000-0000-0000-000000000001" { oa_activity a "A" id "00000000-0000-0000-0000-000000000001" {} }"#, "Duplicate identity", ); rejected( r#"arcadia 2; project mission "x" { diagram view_one "View" {} oa_activity a "A" {} node a { diagram @view_one; element @a; } }"#, "Duplicate alias", ); rejected( r#"arcadia 2; project mission "x" { oa_activity mission "A" {} }"#, "Duplicate alias", ); } #[test] fn typed_scenario_messages_values_and_reuse_round_trip_in_both_work_perspectives() { for (prefix, work, exchange, viewpoint) in [ ("oa", "activity", "interaction", "OperationalActivity"), ("sa", "function", "functional_exchange", "Functional"), ] { let source = format!( r#"arcadia 2; project messages "Typed exchanges and reuse" {{ {prefix}_{work} send_work "Send request" {{}} {prefix}_{work} receive_work "Handle request" {{}} {prefix}_exchange_item payload "Request payload" {{}} {prefix}_data_field payload_value "Requested depth" {{ parent @payload; type_ref Real; unit "m"; }} {exchange} request_exchange "Request" {{ source @send_work; target @receive_work; items [@payload]; }} {prefix}_scenario reusable_story "Reusable request" {{ viewpoint {viewpoint}; steps [@nested_message]; }} {prefix}_lifeline nested_sender "Sender" {{ parent @reusable_story; represented @send_work; }} {prefix}_lifeline nested_receiver "Receiver" {{ parent @reusable_story; represented @receive_work; }} {prefix}_occurrence nested_message "Asynchronous request" {{ parent @reusable_story; body {{ Exchange {{ exchange @request_exchange; sender @nested_sender; receiver @nested_receiver; message_kind Asynchronous; reply_to none; values []; }}; }}; }} {prefix}_scenario story "Request lifecycle" {{ viewpoint {viewpoint}; steps [@created, @called, @replied, @waited, @reused_story, @deleted]; }} {prefix}_lifeline sender_line "Requester" {{ parent @story; represented @send_work; }} {prefix}_lifeline receiver_line "Responder" {{ parent @story; represented @receive_work; }} {prefix}_occurrence created "Create responder" {{ parent @story; body {{ Exchange {{ exchange @request_exchange; sender @sender_line; receiver @receiver_line; message_kind Create; reply_to none; values []; }}; }}; }} {prefix}_occurrence called "Call responder" {{ parent @story; body {{ Exchange {{ exchange @request_exchange; sender @sender_line; receiver @receiver_line; message_kind Call; reply_to none; values [{{ field @payload_value; expression "requested_depth + 2"; }}]; }}; }}; }} {prefix}_occurrence replied "Return response" {{ parent @story; body {{ Exchange {{ exchange @request_exchange; sender @receiver_line; receiver @sender_line; message_kind Reply; reply_to @called; values []; }}; }}; }} {prefix}_occurrence waited "Wait for next request" {{ parent @story; body {{ Exchange {{ exchange none; sender @sender_line; receiver none; message_kind {{ Timer {{ expression "after 5 s"; }}; }}; reply_to none; values []; }}; }}; }} {prefix}_occurrence reused_story "Reuse request" {{ parent @story; body {{ ScenarioUse {{ scenario @reusable_story; bindings [{{ referenced @nested_sender; local @sender_line; }}, {{ referenced @nested_receiver; local @receiver_line; }}]; }}; }}; }} {prefix}_occurrence deleted "Delete responder" {{ parent @story; body {{ Exchange {{ exchange @request_exchange; sender @sender_line; receiver @receiver_line; message_kind Delete; reply_to none; values []; }}; }}; }} }}"# ); let model = read(&source); let canonical = language::format(&model); for field in [ "reply_to @called;", "field @payload_value;", "scenario @reusable_story;", "referenced @nested_sender;", "local @sender_line;", ] { assert!( canonical.contains(field), "missing readable reference {field}" ); } assert_eq!(read(&canonical), model); assert_eq!( persistence::decode(&serde_json::to_vec(&model).unwrap()).unwrap(), model ); assert!( language::parse(&canonical.replace("reply_to @called;", "reply_to @created;")).is_err() ); assert!( language::parse(&canonical.replace("scenario @reusable_story;", "scenario @story;")) .is_err() ); let foreign_viewpoint = if prefix == "oa" { "Functional" } else { "OperationalActivity" }; assert!( language::parse(&canonical.replace( &format!("viewpoint {viewpoint};"), &format!("viewpoint {foreign_viewpoint};") )) .is_err() ); } } #[test] fn typed_events_vertices_and_transition_kinds_round_trip_in_both_layers() { use std::fmt::Write; for (prefix, participant, work) in [("oa", "entity", "activity"), ("sa", "system", "function")] { let mut source = format!( r#"arcadia 2; project recovery "Recovery — безопасно" {{ {prefix}_{participant} owner "Owner" {{}} {prefix}_{work} assess "Assess recovery" {{}} {prefix}_exchange_item payload "Recovery report" {{}} {prefix}_event report "Report received" {{ body {{ Signal {{ items [@payload]; }}; }}; }} {prefix}_event deadline "Recovery deadline" {{ body {{ Time {{ kind After; expression "30 s"; }}; }}; }} {prefix}_event scheduled "Planned handover" {{ body {{ Time {{ kind At; expression "2026-09-05T18:00:00Z"; }}; }}; }} {prefix}_event changed "Pressure recovered" {{ body {{ Change {{ expression "pressure > 4 && status == \"Ready\""; }}; }}; }} {prefix}_state_machine machine "Recovery machine" {{ parent @owner; }} {prefix}_region region "Main region" {{ parent @machine; }} {prefix}_initial initial "Initial" {{ parent @region; }} {prefix}_state idle "Idle" {{ parent @region; }} {prefix}_state active "Active" {{ parent @region; }} {prefix}_region nested "Active region" {{ parent @active; }} {prefix}_state recovering "Recovering" {{ parent @nested; }} state_transition start "Start" {{ source @initial; target @idle; }} state_transition recover "Recover" {{ source @idle; target @active; transition_kind External; triggers [@deadline, @assess]; effects [@assess]; trigger "Operator confirms\nНе запускать код"; guard "available && status == \"Ready\""; effect "Record the assessment"; }} state_transition keep_active "Handle report" {{ source @active; target @active; transition_kind Internal; triggers [@report, @scheduled, @changed]; effects []; }} state_transition enter_recovery "Enter recovery" {{ source @active; target @recovering; transition_kind Local; triggers []; effects []; }} "# ); for stem in [ "choice", "junction", "fork", "join", "shallow_history", "deep_history", "entry_point", "exit_point", "terminate", ] { writeln!( source, "{prefix}_{stem} vertex_{stem} \"{stem}\" {{ parent @region; }}" ) .unwrap(); } source.push('}'); let model = read(&source); let canonical = language::format(&model); for fragment in [ "transition_kind External;", "transition_kind Local;", "transition_kind Internal;", "triggers [@deadline, @assess];", "effects [@assess];", "items [@payload];", "kind After;", "kind At;", ] { assert!(canonical.contains(fragment), "missing {fragment}"); } assert_eq!(read(&canonical), model); assert_eq!( persistence::decode(&serde_json::to_vec(&model).unwrap()).unwrap(), model ); let transition = model .relations .values() .find(|r| r.alias == "recover") .unwrap(); let RelationBody::StateTransition { trigger, guard, effect, .. } = &transition.body else { panic!("expected transition") }; assert_eq!(trigger, "Operator confirms\nНе запускать код"); assert_eq!(guard, "available && status == \"Ready\""); assert_eq!(effect, "Record the assessment"); for invalid in [ canonical.replace("transition_kind External;", "transition_kind Unsupported;"), canonical.replace( "triggers [@deadline, @assess];", "triggers [\"deadline\", @assess];", ), canonical.replace( "triggers [@deadline, @assess];", "triggers [@deadline, @deadline];", ), canonical.replace("effects [@assess];", "effects [@payload];"), canonical.replace("kind After;", "kind Relative;"), canonical.replace("items [@payload];", "items [@assess];"), ] { assert!( language::parse(&invalid).is_err(), "invalid typed state/event source accepted" ); } let legacy = canonical .replace("transition_kind External;", "") .replace("triggers [@deadline, @assess];", "") .replace("effects [@assess];", ""); let legacy = read(&legacy); let RelationBody::StateTransition { transition_kind, triggers, effects, trigger: retained_trigger, guard: retained_guard, effect: retained_effect, .. } = &legacy.relations[&transition.id].body else { panic!("expected transition") }; assert_eq!(*transition_kind, TransitionKind::External); assert!(triggers.is_empty() && effects.is_empty()); assert_eq!( (retained_trigger, retained_guard, retained_effect), (trigger, guard, effect) ); let draft = canonical.replace("expression \"30 s\";", "expression \"\";"); assert!(language::parse(&draft).is_ok()); let deadline = model .elements .values() .find(|e| e.alias == "deadline") .unwrap() .id; let diagnostics = language::lint(&draft); let warning = diagnostics .iter() .find(|d| d.code == "missing_event_expression") .unwrap(); assert_eq!(warning.severity, validation::Severity::Warning); assert_eq!(warning.object_id, Some(deadline)); assert!(warning.line > 0 && warning.column > 0); } } #[test] fn original_v2_text_and_json_preserve_identity_and_mixed_scenarios_after_extensions() { // Golden V2 files predate explicit viewpoints and message semantics. Do not // regenerate these fixtures from the current demo or canonical printer. let old_source = include_str!("fixtures/v2-coastal-rescue.arcadia"); let old_json = include_bytes!("fixtures/v2-coastal-rescue.json"); assert!(!old_source.contains("viewpoint")); assert!(!old_source.contains("message_kind")); assert!(!old_source.contains("transition_kind")); let from_source = read(old_source); let from_json = persistence::decode(old_json).unwrap(); 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"], 3); retained["schema_version"] = 2.into(); for element in retained["elements"].as_object_mut().unwrap().values_mut() { for layer in ["Oa", "Sa"] { let Some(shared) = element["body"] .get_mut(layer) .and_then(|body| body.get_mut("Shared")) else { continue; }; if let Some(port) = shared.get_mut("Port") { assert_eq!(port["contract"], "Legacy"); *port = port["legacy"].clone(); } if let Some(scenario) = shared .get_mut("Scenario") .and_then(serde_json::Value::as_object_mut) { scenario.remove("viewpoint"); } if let Some(exchange) = shared .get_mut("Occurrence") .and_then(|body| body.get_mut("body")) .and_then(|body| body.get_mut("Exchange")) .and_then(serde_json::Value::as_object_mut) { for added in ["message_kind", "reply_to", "values"] { exchange.remove(added); } } } } for relation in retained["relations"].as_object_mut().unwrap().values_mut() { if let Some(boundary) = relation["body"].get_mut("BoundaryExchange") { assert_eq!(boundary["oriented"], true); boundary.as_object_mut().unwrap().remove("oriented"); } if let Some(transition) = relation["body"] .get_mut("StateTransition") .and_then(serde_json::Value::as_object_mut) { for added in ["transition_kind", "triggers", "effects"] { transition.remove(added); } } } assert_eq!( retained, original, "every original V2 field survives; only explicit new compatibility defaults are added" ); assert_eq!(from_json.id.to_string(), original["id"].as_str().unwrap()); assert_eq!( from_json.elements.len(), original["elements"].as_object().unwrap().len() ); assert_eq!( from_json.relations.len(), original["relations"].as_object().unwrap().len() ); assert_eq!( serde_json::to_value(&from_json.diagrams).unwrap(), original["diagrams"] ); assert_eq!( serde_json::to_value(&from_json.traces).unwrap(), original["traces"] ); assert_eq!( serde_json::to_value(&from_json.artifact_links).unwrap(), original["artifact_links"] ); let mut scenarios = 0; for element in from_source.elements.values() { if let Some(SharedBody::Scenario { viewpoint, .. }) = element.shared() { scenarios += 1; assert_eq!(*viewpoint, ScenarioViewpoint::MixedLegacy); } if let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind, reply_to, values, .. }, .. }) = element.shared() { assert_eq!(*message_kind, MessageKind::Asynchronous); assert_eq!(*reply_to, None); assert!(values.is_empty()); } let before = &original["elements"][element.id.to_string()]; assert_eq!(element.alias, before["alias"].as_str().unwrap()); assert_eq!(element.name, before["name"].as_str().unwrap()); assert_eq!( serde_json::to_value(element.parent).unwrap(), before["parent"] ); } assert!( scenarios > 0, "fixture must cover actual old scenario content" ); let canonical = language::format(&from_source); assert!(canonical.contains("viewpoint MixedLegacy;")); for relation in from_source.relations.values() { if let RelationBody::StateTransition { transition_kind, triggers, effects, .. } = &relation.body { assert_eq!(*transition_kind, TransitionKind::External); assert!(triggers.is_empty() && effects.is_empty()); } } assert_eq!(read(&canonical), from_source); } #[test] fn v1_text_and_json_migrate_deterministically_without_losing_legacy_semantics() { let text = include_str!("../../../examples/legacy-v1-coastal-rescue.arcadia"); let json = include_bytes!("../../../examples/legacy-v1-coastal-rescue.oa.json"); let from_text = read(text); let from_json = persistence::decode(json).unwrap(); 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, 3); 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() { let id = Id::parse_str(id).unwrap(); let e = &from_text.elements[&id]; assert_eq!(e.name, record["name"].as_str().unwrap()); assert_eq!(e.description, record["description"].as_str().unwrap()); let n = from_text .diagrams .values() .flat_map(|d| d.nodes.values()) .find(|n| n.element == id) .unwrap(); assert_eq!( serde_json::to_value(n.position).unwrap(), record["position"] ); if record["kind"] == "Process" { let steps = e.steps().unwrap(); for (occurrence, exchange) in steps.iter().zip(record["steps"].as_array().unwrap()) { assert!( matches!(from_text.elements[occurrence].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::Exchange{exchange:Some(actual),..},..})if actual.to_string()==exchange.as_str().unwrap()) ); } } } assert_eq!(read(&language::format(&from_text)), from_text); } #[test] fn v1_repeated_exchange_occurrences_and_authored_aliases_survive() { let source = r#"arcadia 1; project "Legacy" id "00000000-0000-0000-0000-000000000001" { activity first_custom "A" id "00000000-0000-0000-0000-000000000002" {} activity second_custom "B" id "00000000-0000-0000-0000-000000000003" {} interaction report_custom "Report" id "00000000-0000-0000-0000-000000000004" from first_custom to second_custom; process story_custom "Story" id "00000000-0000-0000-0000-000000000005" { position (-0, 12.5); steps [report_custom, report_custom]; } }"#; let p = read(source); assert_eq!(p, read(source)); let process = p .elements .values() .find(|e| e.alias == "story_custom") .unwrap(); let steps = process.steps().unwrap(); assert_ne!(steps[0], steps[1]); assert_eq!( p.elements[&steps[0]].shared(), p.elements[&steps[1]].shared() ); assert!(p.relations.values().any(|r| r.alias == "report_custom")); let node = p .diagrams .values() .flat_map(|d| d.nodes.values()) .find(|n| n.element == process.id) .unwrap(); assert_eq!(node.position.x.to_bits(), (-0.0_f32).to_bits()); } #[test] fn malformed_json_never_ignores_duplicate_maps_or_nested_unknown_fields() { let p = complete_model(); let json = serde_json::to_string(&p).unwrap(); let record = &p.elements.values().next().unwrap(); let encoded = serde_json::to_string(record).unwrap(); let duplicate = json.replacen( &std::format!("\"{}\":{encoded}", record.id), &std::format!("\"{}\":{encoded},\"{}\":{encoded}", record.id, record.id), 1, ); assert!(persistence::decode(duplicate.as_bytes()).is_err()); let mut value = serde_json::to_value(&p).unwrap(); let item = p .elements .values() .find(|e| e.kind() == Kind::ExchangeItem && e.layer() == Layer::Sa) .unwrap(); value["elements"][item.id.to_string()]["body"]["Sa"]["Shared"]["ExchangeItem"]["surprise"] = true.into(); assert!(persistence::decode(&serde_json::to_vec(&value).unwrap()).is_err()); } #[test] fn coordinate_bits_and_permitted_strings_survive_text_and_storage() { let mut p = complete_model(); let values = [ 0., -0., f32::from_bits(1), -f32::from_bits(1), -12.375, 999_999.94, -1_000_000., ]; for value in values { let node = p .diagrams .values_mut() .find_map(|d| d.nodes.values_mut().next()) .unwrap(); node.position = Position { x: value, y: value }; let id = node.id; let parsed = read(&language::format(&p)); let node = parsed .diagrams .values() .find_map(|d| d.nodes.get(&id)) .unwrap(); assert_eq!(node.position.x.to_bits(), value.to_bits()); } p.description = "Unicode é e\u{301} 海 🌊\n\r\t\\\" <>&".into(); assert_eq!(read(&language::format(&p)), p); let temp = tempfile::tempdir().unwrap(); for name in ["model.arcadia", "model.json"] { let path = temp.path().join(name); persistence::save(&path, &p).unwrap(); assert_eq!(persistence::load(&path).unwrap(), p); } let path = temp.path().join("model.json"); let before = std::fs::read(&path).unwrap(); p.description.push('\0'); assert!(persistence::save(&path, &p).is_err()); assert_eq!(std::fs::read(&path).unwrap(), before); } #[test] fn tolerant_analysis_covers_unicode_and_returns_declarations_before_failure() { use language::TokenClass; for source in [ "", "// 検査 🌊\narcadia 2;", "oa_activity assess \"unfinished 🌊", "parent @coordination; guard \"x\\q\";", "}\u{000c}☃\t-2.5e-3 [none, true, false]", ] { let mut end = 0; for token in language::tokenize(source) { assert_eq!(token.span.start, end); assert!(token.span.end > token.span.start); assert!(source.is_char_boundary(token.span.start)); assert!(source.is_char_boundary(token.span.end)); end = token.span.end; } assert_eq!(end, source.len()); } assert_eq!(language::tokenize("// 🌊")[0].class, TokenClass::Comment); let source = "arcadia 2;\nproject mission \"測試 🌊\" {\n oa_activity work \"Work\" id \"00000000-0000-0000-0000-000000000001\" {}\n oa_activity bad \"Bad\" { parent @unknown; }\n}"; let analysis = language::analyze(source); assert!( analysis .declarations .iter() .any(|d| d.alias == "work" && d.id == Some(Id::from_u128(1))) ); let error = &analysis.diagnostics[0]; let offset = source.find("@unknown").unwrap(); assert_eq!(error.span.start, offset); assert_eq!(error.line, 4); assert_eq!( error.column, source[..offset] .rsplit('\n') .next() .unwrap() .chars() .count() + 1 ); } #[test] fn text_and_model_transactions_preserve_history_with_stable_aliases() { let mut session = Session::new(complete_model()); let original = session.project().clone(); let edited = language::format(&original).replace("Harbor safety — 港 🌊", "Updated mission"); session.execute(Command::Replace(read(&edited))).unwrap(); assert!(session.dirty()); assert_eq!(session.project().name, "Updated mission"); session.undo(); assert_eq!(session.project(), &original); assert!(!session.dirty()); session.redo(); assert_eq!(session.project().name, "Updated mission"); } #[test] fn complete_normative_examples_are_accepted_by_the_real_parser() { let spec = include_str!("../../../docs/language.rst"); let mut examples = 0; for section in spec.split(".. code-block:: text").skip(1) { let mut lines = section.lines().skip_while(|line| line.trim().is_empty()); let mut code = String::new(); for line in lines.by_ref() { if let Some(indented) = line.strip_prefix(" ") { code.push_str(indented); code.push('\n'); } else if line.is_empty() { code.push('\n'); } else { break; } } if code.trim_start().starts_with("arcadia 2;") || code.trim_start().starts_with("arcadia 3;") { read(&code); examples += 1; } } assert!( examples > 0, "The normative specification must include a complete authoring example." ); }