Add reproducible three-participant routing workshop

This commit is contained in:
2026-09-05 20:48:39 +03:00
parent 30c7db105a
commit 4b8545d57d
3 changed files with 5787 additions and 0 deletions
+579
View File
@@ -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