//! 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 { 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 { 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> = 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, relations: BTreeMap, } 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 { 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::>() .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, ) { 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, ) { 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, ) { 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, Vec) { 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."] ); } }