Add reproducible three-participant routing workshop
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
//! A repeatable native routing exercise built on the complete coastal-rescue model.
|
||||
//! cargo run --locked -p oa-core --example routing_workshop [output-directory]
|
||||
use oa_core::*;
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
fn identity(alias: &str) -> Id {
|
||||
Id::new_v5(
|
||||
&Id::NAMESPACE_URL,
|
||||
format!("urn:arcadia-studio:routing-workshop:{alias}").as_bytes(),
|
||||
)
|
||||
}
|
||||
fn find(p: &Project, alias: &str) -> Id {
|
||||
p.identities()
|
||||
.into_iter()
|
||||
.find(|(_, name)| *name == alias)
|
||||
.unwrap_or_else(|| panic!("Missing example object: {alias}"))
|
||||
.0
|
||||
}
|
||||
fn stable_identities(p: &Project) -> Project {
|
||||
// Demo placement aliases otherwise use random suffixes when an element is
|
||||
// shown in several views. Their diagram + element pair is the durable key.
|
||||
let mut commands = vec![];
|
||||
for d in p.diagrams.values() {
|
||||
for n in d.nodes.values() {
|
||||
commands.push(Command::SetAlias(
|
||||
n.id,
|
||||
format!("{}_{}", d.alias, p.elements[&n.element].alias),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut stable = execute_batch(p, &commands).unwrap();
|
||||
let ids: Vec<_> = stable
|
||||
.identities()
|
||||
.into_iter()
|
||||
.map(|(id, alias)| (id, identity(alias)))
|
||||
.filter(|(old, new)| old != new)
|
||||
.collect();
|
||||
for (old, new) in ids {
|
||||
stable = execute_batch(&stable, &[Command::Reidentify(old, new)]).unwrap();
|
||||
}
|
||||
stable
|
||||
}
|
||||
fn add(
|
||||
p: &mut Project,
|
||||
kind: Kind,
|
||||
layer: Layer,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
parent: Option<Id>,
|
||||
) -> Id {
|
||||
let mut element = p.make_element(kind, layer, name, parent).unwrap();
|
||||
element.id = identity(alias);
|
||||
element.alias = alias.into();
|
||||
p.elements.insert(element.id, element);
|
||||
identity(alias)
|
||||
}
|
||||
fn relation(
|
||||
p: &mut Project,
|
||||
kind: RelationKind,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
source: Id,
|
||||
target: Id,
|
||||
) -> Id {
|
||||
let id = identity(alias);
|
||||
p.relations.insert(
|
||||
id,
|
||||
Relation {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
body: RelationBody::new(kind, source, target),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
fn group(
|
||||
p: &mut Project,
|
||||
layer: Layer,
|
||||
owner: Id,
|
||||
parent: Id,
|
||||
existing: Option<Id>,
|
||||
prefix: &str,
|
||||
names: [&str; 3],
|
||||
) -> [Id; 3] {
|
||||
let kind = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let root = add(
|
||||
p,
|
||||
kind,
|
||||
layer,
|
||||
&format!("{prefix}_work"),
|
||||
names[0],
|
||||
Some(parent),
|
||||
);
|
||||
let first = existing.unwrap_or_else(|| {
|
||||
add(
|
||||
p,
|
||||
kind,
|
||||
layer,
|
||||
&format!("{prefix}_first"),
|
||||
names[1],
|
||||
Some(root),
|
||||
)
|
||||
});
|
||||
let first_element = p.elements.get_mut(&first).unwrap();
|
||||
first_element.parent = Some(root);
|
||||
first_element.name = names[1].into();
|
||||
let second = add(
|
||||
p,
|
||||
kind,
|
||||
layer,
|
||||
&format!("{prefix}_second"),
|
||||
names[2],
|
||||
Some(root),
|
||||
);
|
||||
for (index, work) in [first, second].into_iter().enumerate() {
|
||||
if !p.relations.values().any(|r| {
|
||||
r.kind() == RelationKind::Allocation && r.source() == owner && r.target() == work
|
||||
}) {
|
||||
relation(
|
||||
p,
|
||||
RelationKind::Allocation,
|
||||
&format!("{prefix}_allocation_{index}"),
|
||||
&format!("{} performs {}", p.named(owner), p.named(work)),
|
||||
owner,
|
||||
work,
|
||||
);
|
||||
}
|
||||
}
|
||||
[root, first, second]
|
||||
}
|
||||
fn interaction(p: &mut Project, alias: &str, name: &str, source: Id, target: Id, item: Id) -> Id {
|
||||
let id = relation(p, RelationKind::Interaction, alias, name, source, target);
|
||||
p.relations.get_mut(&id).unwrap().body = RelationBody::Interaction {
|
||||
source,
|
||||
target,
|
||||
items: vec![item],
|
||||
};
|
||||
id
|
||||
}
|
||||
fn functional(p: &mut Project, alias: &str, name: &str, source: Id, target: Id, item: Id) -> Id {
|
||||
let request = FunctionalExchangeRequest {
|
||||
id: identity(alias),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
source,
|
||||
target,
|
||||
source_port: PortChoice::Auto,
|
||||
target_port: PortChoice::Auto,
|
||||
items: vec![item],
|
||||
};
|
||||
*p = execute_batch(p, &plan_functional_exchange(p, &request).unwrap()).unwrap();
|
||||
request.id
|
||||
}
|
||||
fn boundary(p: &mut Project, fe: Id, alias: &str, name: &str, kind: ComponentPortKind) {
|
||||
let request = ComponentExchangeRequest {
|
||||
functional_exchange: fe,
|
||||
boundary: ComponentExchangeChoice::Create {
|
||||
id: identity(alias), alias: alias.into(), name: name.into(),
|
||||
description: "Component exchange explicitly implements its function exchange through allocated typed ports.".into(),
|
||||
port_kind: kind, oriented: kind == ComponentPortKind::Flow,
|
||||
source_port: PortChoice::Create, target_port: PortChoice::Create,
|
||||
},
|
||||
};
|
||||
*p = execute_batch(p, &plan_component_exchange(p, &request).unwrap()).unwrap();
|
||||
}
|
||||
fn architecture(p: &mut Project, layer: Layer, participants: [Id; 3], groups: [[Id; 3]; 3]) {
|
||||
let diagram = p
|
||||
.diagrams
|
||||
.values()
|
||||
.find(|d| d.layer == layer && d.projection == ProjectionKind::Allocation)
|
||||
.unwrap()
|
||||
.id;
|
||||
let d = p.diagrams.get_mut(&diagram).unwrap();
|
||||
d.name = format!("{layer} routing workshop");
|
||||
d.description = "Three participants: vessel, coastal relay, rescue team. Direct vessel-to-rescue exchanges pass around the relay. Select an exchange and drag a segment to add a bend; move its label, then undo. The incident decision stays inside Rescue: move the whole container to carry both endpoint pins. Fold/unfold and save/reopen to check preservation. Allocation summaries are nested inside participants; semantic decomposition remains separate.".into();
|
||||
d.nodes.clear();
|
||||
d.routes.clear();
|
||||
for (index, (participant, works)) in participants.into_iter().zip(groups).enumerate() {
|
||||
let x = 40. + index as f32 * 340.;
|
||||
for (element, position, size) in [
|
||||
(
|
||||
participant,
|
||||
Position { x, y: 40. },
|
||||
Size {
|
||||
width: 272.,
|
||||
height: 600.,
|
||||
},
|
||||
),
|
||||
(
|
||||
works[0],
|
||||
Position {
|
||||
x: x + 24.,
|
||||
y: 112.,
|
||||
},
|
||||
Size {
|
||||
width: 224.,
|
||||
height: 384.,
|
||||
},
|
||||
),
|
||||
(
|
||||
works[1],
|
||||
Position {
|
||||
x: x + 48.,
|
||||
y: 184.,
|
||||
},
|
||||
Size {
|
||||
width: 176.,
|
||||
height: 132.,
|
||||
},
|
||||
),
|
||||
(
|
||||
works[2],
|
||||
Position {
|
||||
x: x + 48.,
|
||||
y: 344.,
|
||||
},
|
||||
Size {
|
||||
width: 176.,
|
||||
height: 132.,
|
||||
},
|
||||
),
|
||||
] {
|
||||
let id = p.place(diagram, element, position);
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.size = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn trace(p: &mut Project, oa: Id, sa: Id, alias: &str, kind: TraceKind) {
|
||||
let id = identity(alias);
|
||||
p.traces.insert(
|
||||
id,
|
||||
Trace {
|
||||
id,
|
||||
alias: alias.into(),
|
||||
name: format!("{} ↔ {}", p.named(oa), p.named(sa)),
|
||||
description: "OA/SA correspondence in the routing workshop.".into(),
|
||||
oa,
|
||||
sa,
|
||||
kind,
|
||||
baseline: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
fn workshop() -> Project {
|
||||
let mut p = stable_identities(&Project::demo());
|
||||
p.name = "Coastal rescue · Routing workshop".into();
|
||||
p.description = "A three-participant OA/SA exercise for readable connection routing. Begin in an allocation view. Direct exchanges detour around the coastal relay; nested work belongs to its actual performer. SA includes typed functional endpoints, Flow and Standard component exchanges, explicit port/exchange allocations and an internal incident-decision exchange. Add bends and move labels, drag one endpoint or the entire Rescue container, fold/unfold, undo, and save/reopen. Other original engineering views and behavior remain available.".into();
|
||||
let oa_root = find(&p, "respond_to_distress");
|
||||
let sa_root = find(&p, "manage_distress_response");
|
||||
let oa_vessel = find(&p, "vessel_in_distress");
|
||||
let oa_rescue = find(&p, "rescue_coordination");
|
||||
let sa_vessel = find(&p, "vessel_communication_service");
|
||||
let sa_rescue = find(&p, "rescue_coordination_system");
|
||||
let oa_report = find(&p, "report_distress");
|
||||
let oa_assess = find(&p, "assess_incident");
|
||||
let sa_report = find(&p, "transmit_distress_report");
|
||||
let sa_assess = find(&p, "classify_incident_and_alert_operator");
|
||||
let oa_report_item = find(&p, "distress_report");
|
||||
let sa_report_item = find(&p, "digital_distress_report");
|
||||
let oa_relay = add(
|
||||
&mut p,
|
||||
Kind::Entity,
|
||||
Layer::Oa,
|
||||
"oa_coastal_relay",
|
||||
"Coastal relay",
|
||||
None,
|
||||
);
|
||||
let sa_relay = add(
|
||||
&mut p,
|
||||
Kind::SystemActor,
|
||||
Layer::Sa,
|
||||
"sa_coastal_relay",
|
||||
"Coastal relay",
|
||||
None,
|
||||
);
|
||||
for (id, name) in [
|
||||
(oa_vessel, "Vessel"),
|
||||
(oa_rescue, "Rescue team"),
|
||||
(sa_vessel, "Vessel service"),
|
||||
(sa_rescue, "Rescue system"),
|
||||
] {
|
||||
p.elements.get_mut(&id).unwrap().name = name.into();
|
||||
}
|
||||
let ov = group(
|
||||
&mut p,
|
||||
Layer::Oa,
|
||||
oa_vessel,
|
||||
oa_root,
|
||||
Some(oa_report),
|
||||
"oa_vessel",
|
||||
["Vessel contact", "Report distress", "Confirm advice"],
|
||||
);
|
||||
let or = group(
|
||||
&mut p,
|
||||
Layer::Oa,
|
||||
oa_relay,
|
||||
oa_root,
|
||||
None,
|
||||
"oa_relay",
|
||||
["Coastal traffic", "Forward report", "Forward advice"],
|
||||
);
|
||||
let oc = group(
|
||||
&mut p,
|
||||
Layer::Oa,
|
||||
oa_rescue,
|
||||
oa_root,
|
||||
Some(oa_assess),
|
||||
"oa_rescue",
|
||||
["Rescue response", "Assess incident", "Send advice"],
|
||||
);
|
||||
let sv = group(
|
||||
&mut p,
|
||||
Layer::Sa,
|
||||
sa_vessel,
|
||||
sa_root,
|
||||
Some(sa_report),
|
||||
"sa_vessel",
|
||||
["Vessel contact", "Send distress", "Confirm advice"],
|
||||
);
|
||||
let sr = group(
|
||||
&mut p,
|
||||
Layer::Sa,
|
||||
sa_relay,
|
||||
sa_root,
|
||||
None,
|
||||
"sa_relay",
|
||||
["Coastal traffic", "Forward report", "Forward advice"],
|
||||
);
|
||||
let sc = group(
|
||||
&mut p,
|
||||
Layer::Sa,
|
||||
sa_rescue,
|
||||
sa_root,
|
||||
Some(sa_assess),
|
||||
"sa_rescue",
|
||||
["Rescue response", "Assess incident", "Send advice"],
|
||||
);
|
||||
let oa_advice = add(
|
||||
&mut p,
|
||||
Kind::ExchangeItem,
|
||||
Layer::Oa,
|
||||
"oa_safety_advice",
|
||||
"Safety advice",
|
||||
None,
|
||||
);
|
||||
let sa_advice = add(
|
||||
&mut p,
|
||||
Kind::ExchangeItem,
|
||||
Layer::Sa,
|
||||
"sa_safety_advice",
|
||||
"Safety advice",
|
||||
None,
|
||||
);
|
||||
let mut mapped_exchanges = vec![];
|
||||
let pairs = [
|
||||
(
|
||||
"report_to_relay",
|
||||
"Report to relay",
|
||||
ov[1],
|
||||
or[1],
|
||||
sv[1],
|
||||
sr[1],
|
||||
oa_report_item,
|
||||
sa_report_item,
|
||||
),
|
||||
(
|
||||
"relay_to_rescue",
|
||||
"Relayed report",
|
||||
or[1],
|
||||
oc[1],
|
||||
sr[1],
|
||||
sc[1],
|
||||
oa_report_item,
|
||||
sa_report_item,
|
||||
),
|
||||
(
|
||||
"incident_decision",
|
||||
"Incident decision",
|
||||
oc[1],
|
||||
oc[2],
|
||||
sc[1],
|
||||
sc[2],
|
||||
oa_advice,
|
||||
sa_advice,
|
||||
),
|
||||
(
|
||||
"advice_to_relay",
|
||||
"Advice to relay",
|
||||
oc[2],
|
||||
or[2],
|
||||
sc[2],
|
||||
sr[2],
|
||||
oa_advice,
|
||||
sa_advice,
|
||||
),
|
||||
(
|
||||
"advice_to_vessel",
|
||||
"Relayed advice",
|
||||
or[2],
|
||||
ov[2],
|
||||
sr[2],
|
||||
sv[2],
|
||||
oa_advice,
|
||||
sa_advice,
|
||||
),
|
||||
(
|
||||
"direct_advice",
|
||||
"Direct advice",
|
||||
oc[2],
|
||||
ov[2],
|
||||
sc[2],
|
||||
sv[2],
|
||||
oa_advice,
|
||||
sa_advice,
|
||||
),
|
||||
];
|
||||
for (key, name, oa_source, oa_target, sa_source, sa_target, oa_item, sa_item) in pairs {
|
||||
let oa = interaction(
|
||||
&mut p,
|
||||
&format!("oa_{key}"),
|
||||
name,
|
||||
oa_source,
|
||||
oa_target,
|
||||
oa_item,
|
||||
);
|
||||
let sa = functional(
|
||||
&mut p,
|
||||
&format!("sa_{key}"),
|
||||
name,
|
||||
sa_source,
|
||||
sa_target,
|
||||
sa_item,
|
||||
);
|
||||
mapped_exchanges.push((oa, sa, format!("trace_{key}")));
|
||||
}
|
||||
for (fe, alias, name, kind) in [
|
||||
(
|
||||
identity("sa_report_to_relay"),
|
||||
"vessel_relay_channel",
|
||||
"Vessel relay channel",
|
||||
ComponentPortKind::Flow,
|
||||
),
|
||||
(
|
||||
identity("sa_relay_to_rescue"),
|
||||
"relay_rescue_service",
|
||||
"Relay rescue service",
|
||||
ComponentPortKind::Standard,
|
||||
),
|
||||
(
|
||||
identity("sa_advice_to_relay"),
|
||||
"rescue_advice_service",
|
||||
"Rescue advice service",
|
||||
ComponentPortKind::Standard,
|
||||
),
|
||||
(
|
||||
identity("sa_advice_to_vessel"),
|
||||
"relay_vessel_channel",
|
||||
"Relay vessel channel",
|
||||
ComponentPortKind::Flow,
|
||||
),
|
||||
] {
|
||||
boundary(&mut p, fe, alias, name, kind);
|
||||
}
|
||||
trace(
|
||||
&mut p,
|
||||
oa_relay,
|
||||
sa_relay,
|
||||
"trace_coastal_relay",
|
||||
TraceKind::Participant,
|
||||
);
|
||||
trace(
|
||||
&mut p,
|
||||
oa_advice,
|
||||
sa_advice,
|
||||
"trace_safety_advice",
|
||||
TraceKind::Item,
|
||||
);
|
||||
for (oa, sa, key) in mapped_exchanges {
|
||||
trace(&mut p, oa, sa, &key, TraceKind::Exchange);
|
||||
}
|
||||
for (oa, sa) in ov
|
||||
.into_iter()
|
||||
.zip(sv)
|
||||
.chain(or.into_iter().zip(sr))
|
||||
.chain(oc.into_iter().zip(sc))
|
||||
{
|
||||
if !p.traces.values().any(|t| t.oa == oa && t.sa == sa) {
|
||||
let key = format!("trace_workshop_{}", p.elements[&oa].alias);
|
||||
trace(&mut p, oa, sa, &key, TraceKind::Refinement);
|
||||
}
|
||||
}
|
||||
for id in p.traces.keys().copied().collect::<Vec<_>>() {
|
||||
let t = &p.traces[&id];
|
||||
let baseline = TraceBaseline {
|
||||
oa: p.properties(t.oa).unwrap(),
|
||||
sa: p.properties(t.sa).unwrap(),
|
||||
};
|
||||
p.traces.get_mut(&id).unwrap().baseline = Some(baseline);
|
||||
}
|
||||
architecture(
|
||||
&mut p,
|
||||
Layer::Oa,
|
||||
[oa_vessel, oa_relay, oa_rescue],
|
||||
[ov, or, oc],
|
||||
);
|
||||
architecture(
|
||||
&mut p,
|
||||
Layer::Sa,
|
||||
[sa_vessel, sa_relay, sa_rescue],
|
||||
[sv, sr, sc],
|
||||
);
|
||||
p = stable_identities(&p);
|
||||
let layouts: Vec<_> = p
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|d| d.projection != ProjectionKind::Allocation)
|
||||
.map(|d| Command::Layout(d.id))
|
||||
.collect();
|
||||
execute_batch(&p, &layouts).unwrap()
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let directory = std::env::args_os()
|
||||
.nth(1)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("examples"));
|
||||
let p = workshop();
|
||||
assert_eq!(
|
||||
p,
|
||||
workshop(),
|
||||
"Workshop identities, aliases and layout must be reproducible"
|
||||
);
|
||||
let issues = validation::validate(&p);
|
||||
assert!(issues.is_empty(), "Workshop model diagnostics: {issues:?}");
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let d = p
|
||||
.diagrams
|
||||
.values()
|
||||
.find(|d| d.layer == layer && d.projection == ProjectionKind::Allocation)
|
||||
.unwrap();
|
||||
let view = projection::project_view(&p, d.id);
|
||||
let routed = routing::route_diagram(&p, d, &view);
|
||||
let warnings: BTreeMap<_, _> = routed
|
||||
.edges
|
||||
.values()
|
||||
.filter(|r| !r.warnings.is_empty())
|
||||
.map(|r| (p.named(r.id), &r.warnings))
|
||||
.collect();
|
||||
println!(
|
||||
"{layer}: {} nested cards, {} exchanges, route warnings: {warnings:?}",
|
||||
view.nodes.len(),
|
||||
routed.edges.len()
|
||||
);
|
||||
}
|
||||
let source = directory.join("routing-workshop.arcadia");
|
||||
let json = directory.join("routing-workshop.json");
|
||||
persistence::save(&source, &p)?;
|
||||
persistence::save(&json, &p)?;
|
||||
assert_eq!(persistence::load(&source)?, p);
|
||||
assert_eq!(persistence::load(&json)?, p);
|
||||
println!(
|
||||
"Wrote and verified {} and {}",
|
||||
source.display(),
|
||||
json.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user