Files
ARCADIA-Studio/crates/oa-desktop/src/port_preview.rs
T

966 lines
35 KiB
Rust

//! Engineering facts for reviewed port operations. This formats the concrete
//! planner commands; it neither infers interface roles nor executes a second edit.
use oa_core::*;
use std::collections::{BTreeMap, BTreeSet};
/// Explain a port plan, including endpoints reused by its concrete commands.
/// An empty plan has no subject, so its existing context belongs in the caller.
pub fn change_rows(before: &Project, commands: &[Command]) -> Vec<String> {
let mut pending: Vec<_> = commands.iter().rev().collect();
let mut flat = Vec::new();
while let Some(command) = pending.pop() {
if let Command::Batch(children) = command {
pending.extend(children.iter().rev());
} else {
flat.push(command);
}
}
if flat.is_empty() {
return vec!["Already represented; no model changes required.".into()];
}
// Overlay borrowed records for names of newly created endpoints, without
// cloning or validating the entire project on every view render.
let mut context = Context::new(before);
for command in &flat {
match command {
Command::CreateElement(e) | Command::EditElement(_, e) => {
context.elements.insert(e.id, e);
}
Command::CreateRelation(r) | Command::EditRelation(_, r) => {
context.relations.insert(r.id, r);
}
_ => {}
}
}
let original = Context::new(before);
let mut rows = Vec::new();
let mut reused_ports = BTreeSet::new();
let mut reused_exchanges = BTreeSet::new();
let mut reused_allocations = BTreeSet::new();
for command in flat {
match command {
Command::CreateElement(e) => port_rows(&context, None, e, "Create", &mut rows),
Command::EditElement(id, e) => {
port_rows(&context, before.elements.get(id), e, "Update", &mut rows);
}
Command::CreateRelation(r) | Command::EditRelation(_, r) => {
let old = if let Command::EditRelation(id, _) = command {
before.relations.get(id)
} else {
None
};
rows.push(format!(
"{} {}: {}",
if old.is_some() { "Update" } else { "Create" },
r.kind(),
context.label(r.id)
));
relation_rows(&context, old, r, &mut rows);
for id in references::relation_references(r) {
if context.element(id).is_some_and(|e| e.kind() == Kind::Port) {
reused_ports.insert(id);
}
}
if let RelationBody::ExchangeAllocation { source, target } = r.body {
// Existing facts omitted by an idempotent planner still
// matter to the reviewed correspondence.
for exchange in [source, target] {
reused_exchanges.insert(exchange);
if let Some(exchange) = context.relation(exchange) {
for id in references::relation_references(exchange) {
if context.element(id).is_some_and(|e| e.kind() == Kind::Port) {
reused_ports.insert(id);
}
}
}
}
if let (Some(boundary), Some(functional)) =
(context.relation(source), context.relation(target))
&& let (
RelationBody::BoundaryExchange {
source: a,
target: b,
..
},
RelationBody::FunctionalExchange {
source_port,
target_port,
..
},
) = (&boundary.body, &functional.body)
{
for allocation in context.all_relations().filter(|allocation| {
matches!(allocation.body, RelationBody::PortAllocation { source, target }
if [*a, *b].contains(&source)
&& [*source_port, *target_port].contains(&target))
}) {
reused_allocations.insert(allocation.id);
}
}
}
}
_ => rows.push(
"Additional command requires separate review; it is outside this port preview."
.into(),
),
}
}
for id in reused_exchanges {
if !context.relations.contains_key(&id)
&& let Some(exchange) = before.relations.get(&id)
{
rows.push(format!("Reuse {}: {}", exchange.kind(), original.label(id)));
relation_rows(&original, None, exchange, &mut rows);
}
}
for id in reused_ports {
if !context.elements.contains_key(&id)
&& let Some(port) = before.elements.get(&id)
{
port_rows(&original, None, port, "Reuse", &mut rows);
}
}
for id in reused_allocations {
if !context.relations.contains_key(&id)
&& let Some(allocation) = before.relations.get(&id)
{
rows.push(format!("Reuse port allocation: {}", original.label(id)));
relation_rows(&original, None, allocation, &mut rows);
}
}
rows
}
/// Explain current item origins and every incident FE before resolution, split,
/// or synchronization. Retained uses refer to the original primary identity.
pub fn port_context_rows(project: &Project, port: Id) -> Vec<String> {
let context = Context::new(project);
let Some(element) = project.elements.get(&port) else {
return vec![format!("Missing port: {port}")];
};
let Some(SharedBody::Port { contract, legacy }) = element.shared() else {
return vec![];
};
let mut rows = vec![format!("Current port: {}", context.endpoint(port))];
let mut origins: BTreeMap<Id, Vec<String>> = BTreeMap::new();
if let Some((side, items)) = function_items(contract) {
rows.push(format!(
"Current {side} declaration: {}",
context.items(items)
));
for item in items {
origins
.entry(*item)
.or_default()
.push(format!("declared {side}"));
}
}
if let Some(legacy) = legacy {
rows.push(legacy_line(&context, legacy));
if let Some(interface) = legacy.interface {
for item in data::interface_items(project, interface) {
origins.entry(item).or_default().push(format!(
"preserved interface {} (suggestion only; not an active typed contract)",
context.label(interface)
));
}
}
}
for relation in project.relations.values() {
if let RelationBody::FunctionalExchange {
source_port,
target_port,
items,
..
} = &relation.body
{
for (endpoint, role) in [(*source_port, "output"), (*target_port, "input")] {
if endpoint == port {
rows.push(format!(
"Affected functional exchange: {} · {role} endpoint {} · carried items: {}",
context.label(relation.id),
context.label(port),
context.items(items)
));
for item in items {
origins.entry(*item).or_default().push(format!(
"carried by {} at this {role} endpoint",
context.label(relation.id)
));
}
}
}
} else if references::relation_references(relation).contains(&port) {
rows.push(format!(
"Retained {}: {}",
relation.kind(),
context.label(relation.id)
));
relation_rows(&context, None, relation, &mut rows);
}
}
for (item, sources) in origins {
rows.push(format!(
"Item origin · {}: {}",
context.label(item),
sources.join("; ")
));
}
for user in project
.elements
.values()
.filter(|e| e.id != port && references::element_references(e).contains(&port))
{
rows.push(format!(
"Retained {} reference: {} → {}",
user.kind(),
context.label(user.id),
context.label(port)
));
}
for artifact in project.artifact_links.values().filter(|a| a.owner == port) {
rows.push(format!(
"Retained artifact: {} · @{} · {:?} · {} · owner {}",
artifact.name,
artifact.alias,
artifact.relation,
artifact.uri,
context.label(port)
));
}
for trace in project
.traces
.values()
.filter(|t| t.oa == port || t.sa == port)
{
rows.push(format!(
"Retained trace: {} · @{} · OA {} → SA {}",
trace.name,
trace.alias,
context.label(trace.oa),
context.label(trace.sa)
));
}
rows
}
struct Context<'a> {
before: &'a Project,
elements: BTreeMap<Id, &'a Element>,
relations: BTreeMap<Id, &'a Relation>,
}
impl<'a> Context<'a> {
fn new(before: &'a Project) -> Self {
Self {
before,
elements: BTreeMap::new(),
relations: BTreeMap::new(),
}
}
fn element(&self, id: Id) -> Option<&Element> {
self.elements
.get(&id)
.copied()
.or_else(|| self.before.elements.get(&id))
}
fn relation(&self, id: Id) -> Option<&Relation> {
self.relations
.get(&id)
.copied()
.or_else(|| self.before.relations.get(&id))
}
fn all_relations(&self) -> impl Iterator<Item = &Relation> {
self.before
.relations
.values()
.filter(|r| !self.relations.contains_key(&r.id))
.chain(self.relations.values().copied())
}
fn label(&self, id: Id) -> String {
if let Some(e) = self.element(id) {
format!("{} · @{}", e.name, e.alias)
} else if let Some(r) = self.relation(id) {
format!("{} · @{}", r.name, r.alias)
} else {
format!("{} · {id}", self.before.named(id))
}
}
fn endpoint(&self, id: Id) -> String {
let mut label = self.label(id);
if let Some(port) = self.element(id).filter(|e| e.kind() == Kind::Port)
&& let Some(owner) = port.parent
{
label.push_str(&format!(" (owner {})", self.label(owner)));
}
label
}
fn items(&self, items: &[Id]) -> String {
if items.is_empty() {
"none (empty contract)".into()
} else {
items
.iter()
.map(|id| self.label(*id))
.collect::<Vec<_>>()
.join(", ")
}
}
}
fn function_items(contract: &PortContract) -> Option<(&'static str, &[Id])> {
match contract {
PortContract::FunctionInput { incoming_items } => Some(("incoming", incoming_items)),
PortContract::FunctionOutput { outgoing_items } => Some(("outgoing", outgoing_items)),
_ => None,
}
}
fn contract_name(contract: &PortContract) -> String {
match contract {
PortContract::FunctionInput { .. } => "Function input".into(),
PortContract::FunctionOutput { .. } => "Function output".into(),
PortContract::Component { kind, orientation } => {
if *kind == ComponentPortKind::Standard {
"Component Standard (no Flow orientation)".into()
} else {
format!("Component Flow · {orientation:?}")
}
}
PortContract::Legacy => "Legacy (unresolved)".into(),
}
}
fn legacy_line(context: &Context<'_>, legacy: &LegacyPortContract) -> String {
format!(
"Preserved generic association: {:?}; interface {}. This does not declare a provided or required role.",
legacy.direction,
legacy
.interface
.map(|id| context.label(id))
.unwrap_or_else(|| "none".into())
)
}
fn role_qualifier(context: &Context<'_>, source: Id) -> &'static str {
match context.element(source).and_then(Element::shared) {
Some(SharedBody::Port { contract, legacy })
if *contract == PortContract::Legacy
|| (contract.is_function() && legacy.is_some()) =>
{
" (preserved compatibility declaration)"
}
_ => "",
}
}
fn item_change_rows(
context: &Context<'_>,
label: &str,
old: Option<&[Id]>,
new: &[Id],
rows: &mut Vec<String>,
) {
match old {
Some(old) if old != new => rows.push(format!(
"{label}: {} → {}",
Context::new(context.before).items(old),
context.items(new)
)),
_ => rows.push(format!("{label}: {}", context.items(new))),
}
}
fn port_rows(
context: &Context<'_>,
old: Option<&Element>,
port: &Element,
verb: &str,
rows: &mut Vec<String>,
) {
let Some(SharedBody::Port { contract, legacy }) = port.shared() else {
rows.push(format!(
"{verb} {}: {}",
port.kind(),
context.label(port.id)
));
return;
};
rows.push(format!("{verb} port: {}", context.endpoint(port.id)));
let previous = old.and_then(port_contract);
if let Some(previous) =
previous.filter(|previous| contract_name(previous) != contract_name(contract))
{
rows.push(format!(
"Port contract: {} → {}",
contract_name(previous),
contract_name(contract)
));
} else {
rows.push(format!("Port contract: {}", contract_name(contract)));
}
if let Some((side, items)) = function_items(contract) {
item_change_rows(
context,
&format!("{side} items"),
previous.and_then(function_items).map(|(_, items)| items),
items,
rows,
);
}
if let Some(legacy) = legacy {
rows.push(legacy_line(context, legacy));
}
for role in context.all_relations().filter(|r| {
r.source() == port.id
&& matches!(
r.kind(),
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement
)
}) {
rows.push(format!(
"{} interface {} · declaration {}{}",
if role.kind() == RelationKind::InterfaceProvision {
"Provides"
} else {
"Requires"
},
context.label(role.target()),
context.label(role.id),
role_qualifier(context, port.id)
));
}
}
fn relation_rows(
context: &Context<'_>,
old: Option<&Relation>,
relation: &Relation,
rows: &mut Vec<String>,
) {
let original = Context::new(context.before);
match &relation.body {
RelationBody::FunctionalExchange {
source,
target,
source_port,
target_port,
items,
} => {
rows.push(format!(
"Functions: {} → {}",
context.label(*source),
context.label(*target)
));
let previous = old.and_then(|r| match r.body {
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => Some((source_port, target_port)),
_ => None,
});
for (label, port, prior) in [
("Output endpoint", *source_port, previous.map(|p| p.0)),
("Input endpoint", *target_port, previous.map(|p| p.1)),
] {
rows.push(if let Some(prior) = prior.filter(|prior| *prior != port) {
format!(
"{label}: {} → {}",
original.endpoint(prior),
context.endpoint(port)
)
} else {
format!("{label}: {}", context.endpoint(port))
});
}
item_change_rows(
context,
"Carried items",
old.map(|r| r.body.items()),
items,
rows,
);
}
RelationBody::BoundaryExchange {
source,
target,
oriented,
items,
} => {
rows.push(format!(
"Component endpoints: {} {} {}",
context.endpoint(*source),
if *oriented { "→" } else { "—" },
context.endpoint(*target)
));
let orientation = |directed| if directed { "directed" } else { "unoriented" };
let previous = old.and_then(|r| match r.body {
RelationBody::BoundaryExchange { oriented, .. } => Some(oriented),
_ => None,
});
rows.push(
if let Some(previous) = previous.filter(|previous| previous != oriented) {
format!(
"Exchange orientation: {} → {}",
orientation(previous),
orientation(*oriented)
)
} else {
format!(
"Exchange orientation: {} (independent of interface roles)",
orientation(*oriented)
)
},
);
item_change_rows(
context,
"Boundary carried items",
old.map(|r| r.body.items()),
items,
rows,
);
}
RelationBody::PortAllocation { source, target } => {
rows.push(format!(
"Component port {} allocates function port {}",
context.endpoint(*source),
context.endpoint(*target)
));
if let (Some(component), Some(function)) = (
context.element(*source).and_then(|port| port.parent),
context.element(*target).and_then(|port| port.parent),
) {
for allocation in context.all_relations().filter(|r| {
r.kind() == RelationKind::Allocation
&& r.source() == component
&& r.target() == function
}) {
rows.push(format!(
"Actual work allocation: {} performs {} · {}",
context.label(component),
context.label(function),
context.label(allocation.id)
));
}
}
}
RelationBody::ExchangeAllocation { source, target } => {
rows.push(format!(
"Component exchange {} implements functional exchange {}",
context.label(*source),
context.label(*target)
));
}
RelationBody::InterfaceProvision { source, target }
| RelationBody::InterfaceRequirement { source, target } => {
rows.push(format!(
"{} {} interface {}{}",
context.endpoint(*source),
if relation.kind() == RelationKind::InterfaceProvision {
"provides"
} else {
"requires"
},
context.label(*target),
role_qualifier(context, *source)
));
}
_ => rows.push(format!(
"{} → {}",
context.label(relation.source()),
context.label(relation.target())
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn add(p: &mut Project, kind: Kind, name: &str) -> Id {
p.add_element(kind, Layer::Sa, name, None).unwrap()
}
fn port(p: &mut Project, owner: Id, name: &str, contract: PortContract) -> Id {
let id = Id::new_v4();
let element = make_port(p, id, owner, name, contract).unwrap();
p.elements.insert(id, element);
id
}
fn exchange(p: &mut Project, source: Id, target: Id, items: Vec<Id>) -> (Id, Vec<Command>) {
let id = Id::new_v4();
let commands = plan_functional_exchange(
p,
&FunctionalExchangeRequest {
id,
alias: p.fresh_alias("report", "exchange", id),
name: "Transfer report".into(),
description: String::new(),
source,
target,
source_port: PortChoice::Auto,
target_port: PortChoice::Auto,
items,
},
)
.unwrap();
(id, commands)
}
struct Fixture {
project: Project,
source: Id,
target: Id,
first: Id,
second: Id,
}
fn fixture() -> Fixture {
let mut p = Project::empty();
let source = add(&mut p, Kind::Function, "Report incident");
let target = add(&mut p, Kind::Function, "Assess incident");
let actor = add(&mut p, Kind::SystemActor, "Operator");
let system = add(&mut p, Kind::System, "Rescue system");
let first = add(&mut p, Kind::ExchangeItem, "Incident report");
let second = add(&mut p, Kind::ExchangeItem, "Assessment request");
p.relate(RelationKind::Allocation, "Reports", actor, source);
p.relate(RelationKind::Allocation, "Assesses", system, target);
port(
&mut p,
source,
"Report output",
PortContract::FunctionOutput {
outgoing_items: vec![first, second],
},
);
port(
&mut p,
target,
"Report input",
PortContract::FunctionInput {
incoming_items: vec![first, second],
},
);
Fixture {
project: p,
source,
target,
first,
second,
}
}
#[test]
fn functional_creation_identifies_new_and_reused_endpoints_and_owners() {
let Fixture {
mut project,
source,
target,
first,
..
} = fixture();
let removed = project
.children(target)
.find(|e| e.kind() == Kind::Port)
.unwrap()
.id;
project.elements.remove(&removed);
let (_, commands) = exchange(&mut project, source, target, vec![first]);
let before = project.clone();
let rows = change_rows(&project, &commands);
let text = rows.join("\n");
assert!(text.contains("Create port:"));
assert!(text.contains(
"Reuse port: Report output · @report_output (owner Report incident · @report_incident)"
));
assert!(text.contains("Output endpoint: Report output · @report_output"));
assert!(
text.contains("Input endpoint:")
&& text.contains("owner Assess incident · @assess_incident")
);
assert!(text.contains("Carried items: Incident report · @incident_report"));
assert!(text.contains("Port contract: Function input"));
assert_eq!(project, before, "Preview must not apply a plan");
assert!(execute_batch(&project, &commands).is_ok());
}
#[test]
fn reused_boundary_shows_item_union_and_existing_allocating_facts() {
let Fixture {
mut project,
source,
target,
first,
second,
} = fixture();
let (fe, commands) = exchange(&mut project, source, target, vec![first]);
project = execute_batch(&project, &commands).unwrap();
let boundary = Id::new_v4();
let generated = plan_component_exchange(
&project,
&ComponentExchangeRequest {
functional_exchange: fe,
boundary: ComponentExchangeChoice::Create {
id: boundary,
alias: "implementation".into(),
name: "Incident interface".into(),
description: "Keep this prose".into(),
port_kind: ComponentPortKind::Standard,
oriented: false,
source_port: PortChoice::Auto,
target_port: PortChoice::Auto,
},
},
)
.unwrap();
project = execute_batch(&project, &generated).unwrap();
let (other_fe, commands) = exchange(&mut project, source, target, vec![second]);
project = execute_batch(&project, &commands).unwrap();
let interface = add(&mut project, Kind::Interface, "Incident service");
project.relate(
RelationKind::InterfaceItemAllocation,
"First item",
interface,
first,
);
project.relate(
RelationKind::InterfaceItemAllocation,
"Second item",
interface,
second,
);
let connection = &project.relations[&boundary];
let (a, b) = (connection.source(), connection.target());
project.relate(
RelationKind::InterfaceRequirement,
"Client requirement",
a,
interface,
);
project.relate(
RelationKind::InterfaceProvision,
"Service provision",
b,
interface,
);
let commands = plan_component_exchange(
&project,
&ComponentExchangeRequest {
functional_exchange: other_fe,
boundary: ComponentExchangeChoice::Reuse(boundary),
},
)
.unwrap();
let rows = change_rows(&project, &commands);
let text = rows.join("\n");
assert!(text.contains("Boundary carried items: Incident report · @incident_report → Incident report · @incident_report, Assessment request · @assessment_request"));
assert!(text.contains("Exchange orientation: unoriented"));
assert!(text.contains("Port contract: Component Standard (no Flow orientation)"));
assert_eq!(
rows.iter()
.filter(|row| row.starts_with("Reuse port allocation:"))
.count(),
2
);
assert!(text.contains("allocates function port Report output · @report_output (owner Report incident · @report_incident)"));
assert!(text.contains("implements functional exchange Transfer report"));
assert!(text.contains("Actual work allocation: Operator · @operator performs Report incident · @report_incident"));
assert!(text.contains("Requires interface Incident service · @incident_service · declaration Client requirement"));
assert!(text.contains("Provides interface Incident service · @incident_service · declaration Service provision"));
assert_eq!(project.relations[&boundary].description, "Keep this prose");
project = execute_batch(&project, &commands).unwrap();
let (third_fe, commands) = exchange(&mut project, source, target, vec![first]);
project = execute_batch(&project, &commands).unwrap();
let commands = plan_component_exchange(
&project,
&ComponentExchangeRequest {
functional_exchange: third_fe,
boundary: ComponentExchangeChoice::Reuse(boundary),
},
)
.unwrap();
let rows = change_rows(&project, &commands);
assert!(
rows.iter()
.any(|row| row == "Reuse Boundary exchange: Incident interface · @implementation")
);
assert!(
!rows
.iter()
.any(|row| row.starts_with("Create Boundary exchange:"))
);
assert_eq!(change_rows(&project, &[Command::Batch(commands)]), rows);
}
#[test]
fn legacy_split_discloses_endpoint_routing_item_origins_and_retained_evidence() {
let Fixture {
mut project,
source,
target,
first,
..
} = fixture();
let interface = add(&mut project, Kind::Interface, "Preserved contract");
project.relate(
RelationKind::InterfaceItemAllocation,
"Allocated item",
interface,
first,
);
let legacy = port(
&mut project,
source,
"Shared legacy endpoint",
PortContract::Legacy,
);
project.elements.get_mut(&legacy).unwrap().body = ElementBody::shared_in(
Layer::Sa,
SharedBody::Port {
contract: PortContract::Legacy,
legacy: Some(LegacyPortContract {
direction: PortDirection::InOut,
interface: Some(interface),
}),
},
);
let input = project
.children(target)
.find(|e| matches!(port_contract(e), Some(PortContract::FunctionInput { .. })))
.unwrap()
.id;
let output = port(
&mut project,
target,
"Reply output",
PortContract::FunctionOutput {
outgoing_items: vec![first],
},
);
let outgoing = project.relate(
RelationKind::FunctionalExchange,
"Outgoing report",
source,
target,
);
project.relations.get_mut(&outgoing).unwrap().body = RelationBody::FunctionalExchange {
source,
target,
source_port: legacy,
target_port: input,
items: vec![first],
};
let incoming = project.relate(
RelationKind::FunctionalExchange,
"Incoming report",
target,
source,
);
project.relations.get_mut(&incoming).unwrap().body = RelationBody::FunctionalExchange {
source: target,
target: source,
source_port: output,
target_port: legacy,
items: vec![first],
};
let constraint = add(&mut project, Kind::Constraint, "Bandwidth budget");
project.elements.get_mut(&constraint).unwrap().body = ElementBody::shared_in(
Layer::Sa,
SharedBody::Constraint {
expression: "Bandwidth is bounded".into(),
constrained: vec![legacy],
},
);
let artifact = Id::new_v4();
project.artifact_links.insert(
artifact,
ArtifactLink {
id: artifact,
alias: "schematic".into(),
name: "Electrical schematic".into(),
description: String::new(),
owner: legacy,
relation: ArtifactRelation::References,
uri: "./hardware/board.kicad_sch".into(),
revision: None,
fragment: None,
},
);
let activity = project
.add_element(Kind::Activity, Layer::Oa, "Operational reporting", None)
.unwrap();
let trace = Id::new_v4();
project.traces.insert(
trace,
Trace {
id: trace,
alias: "reporting_trace".into(),
name: "Reporting correspondence".into(),
description: String::new(),
oa: activity,
sa: legacy,
kind: TraceKind::Manual,
baseline: None,
},
);
let context = port_context_rows(&project, legacy).join("\n");
assert!(context.contains("Affected functional exchange: Outgoing report"));
assert!(context.contains("Affected functional exchange: Incoming report"));
assert!(
context.contains("Item origin · Incident report · @incident_report:")
&& context.contains("preserved interface Preserved contract")
);
assert!(context.contains("suggestion only; not an active typed contract"));
assert!(context.contains("Retained Constraint reference: Bandwidth budget"));
assert!(context.contains("Retained artifact: Electrical schematic · @schematic"));
assert!(context.contains("Retained trace: Reporting correspondence · @reporting_trace"));
let commands = plan_legacy_work_port_split(
&project,
&LegacyWorkPortSplitRequest {
port: legacy,
primary: FunctionPortSide::Output,
incoming_items: vec![first],
outgoing_items: vec![first],
},
)
.unwrap();
let text = change_rows(&project, &commands).join("\n");
let opposite = Id::new_v5(&legacy, b"split-opposite-port");
let candidate = execute_batch(&project, &commands).unwrap();
assert!(text.contains("Port contract: Legacy (unresolved) → Function output"));
assert!(text.contains(&format!(
"Input endpoint: {} → {}",
Context::new(&project).endpoint(legacy),
Context::new(&candidate).endpoint(opposite)
)));
assert!(text.contains("Create port:") && text.contains("Port contract: Function input"));
assert_eq!(candidate.artifact_links[&artifact].owner, legacy);
assert_eq!(candidate.traces[&trace].sa, legacy);
}
#[test]
fn synchronization_preview_exposes_contract_shrinkage_and_current_declaration_origin() {
let Fixture {
project,
source,
first,
second,
..
} = fixture();
let output = project
.children(source)
.find(|e| e.kind() == Kind::Port)
.unwrap()
.id;
let commands = plan_function_port_items(&project, output, vec![first]).unwrap();
let context = port_context_rows(&project, output).join("\n");
assert!(
context.contains(
"Item origin · Assessment request · @assessment_request: declared outgoing"
)
);
let text = change_rows(&project, &commands).join("\n");
assert!(text.contains("outgoing items: Incident report · @incident_report, Assessment request · @assessment_request → Incident report · @incident_report"));
let candidate = execute_batch(&project, &commands).unwrap();
assert!(
matches!(port_contract(&candidate.elements[&output]), Some(PortContract::FunctionOutput { outgoing_items }) if outgoing_items == &[first])
);
assert!(project.elements.contains_key(&second));
assert_eq!(
change_rows(&project, &[]),
["Already represented; no model changes required."]
);
}
}