Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
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
|
|
}
|