//! 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 }