Replace the oversized header and reference rail with stable editor slots and a retained supporting drawer. Bind diagnostic, result and menu actions to their original publication and visit; preserve effective selections on navigation and dismiss Actions, Find and the drawer one surface per Escape. Retain exact Windows source captures, eight final gates (871 desktop passes), independent reviews, native selection/rename/recovery trials and the original failures. All 526 tested inputs and staged evidence were byte-checked before commit. Combined preview integration and packaging remain with issue 27; Linux remains paused.
1041 lines
28 KiB
Rust
1041 lines
28 KiB
Rust
//! 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>,
|
|
) -> 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<Id>,
|
|
field: Id,
|
|
value: &str,
|
|
) -> OccurrenceBody {
|
|
OccurrenceBody::Exchange {
|
|
operation: None,
|
|
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 {
|
|
operation: None,
|
|
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 {
|
|
operation: None,
|
|
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
|
|
}
|