diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts index 4a8f44b..91407a4 100644 --- a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -41,13 +41,17 @@ import { getCommonAncestor, setLevelOfCSNodes, sortPorts } from "./utils"; * @param idToSNode The map of IDs to SNodes. * @param options The synthesis options of the STPA model. * @param idCache The ID cache of the STPA model. + * @param addMissing Whether missing feedback should be added to the control structure. + * @param missingFeedback The missing feedbacks of the control structure. * @returns the generated control structure diagram. */ export function createControlStructure( controlStructure: Graph, idToSNode: Map, options: StpaSynthesisOptions, - idCache: IdCache + idCache: IdCache, + addMissing: boolean, + missingFeedback?: Map ): ParentNode { // set the level of the nodes in the control structure automatically setLevelOfCSNodes(controlStructure.nodes); @@ -56,7 +60,7 @@ export function createControlStructure( // children (nodes and edges) of the control structure const CSChildren = [ ...csNodes, - ...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache), + ...generateVerticalCSEdges(controlStructure.nodes, idToSNode, idCache, addMissing, missingFeedback), //...this.generateHorizontalCSEdges(filteredModel.controlStructure.edges, idCache) ]; // sort the ports in order to group edges based on the nodes they are connected to @@ -179,47 +183,67 @@ export function createProcessModelNodes(variables: Variable[], idCache: IdCache< * Creates the edges for the control structure. * @param nodes The nodes of the control structure. * @param idCache The ID cache of the STPA model. + * @param addMissing Whether missing feedback should be added to the control structure. + * @param missingFeedback The missing feedbacks of the control structure. * @returns A list of edges for the control structure. */ export function generateVerticalCSEdges( nodes: Node[], idToSNode: Map, - idCache: IdCache + idCache: IdCache, + addMissing: boolean, + missingFeedback?: Map ): (CSNode | CSEdge)[] { const edges: (CSNode | CSEdge)[] = []; // for every control action and feedback of every a node, a edge should be created for (const node of nodes) { // create edges representing the control actions - edges.push(...translateCommandsToEdges(node.actions, EdgeType.CONTROL_ACTION, idToSNode, idCache)); + edges.push( + ...translateCommandsToEdges( + node, + node.actions, + EdgeType.CONTROL_ACTION, + idToSNode, + idCache, + addMissing, + missingFeedback + ) + ); // create edges representing feedback - edges.push(...translateCommandsToEdges(node.feedbacks, EdgeType.FEEDBACK, idToSNode, idCache)); + edges.push(...translateCommandsToEdges(node, node.feedbacks, EdgeType.FEEDBACK, idToSNode, idCache, false)); // create edges representing the other inputs edges.push(...translateIOToEdgeAndNode(node.inputs, node, EdgeType.INPUT, idToSNode, idCache)); // create edges representing the other outputs edges.push(...translateIOToEdgeAndNode(node.outputs, node, EdgeType.OUTPUT, idToSNode, idCache)); // create edges for children and add the ones that must be added at the top level - edges.push(...generateVerticalCSEdges(node.children, idToSNode, idCache)); + edges.push(...generateVerticalCSEdges(node.children, idToSNode, idCache, addMissing, missingFeedback)); } return edges; } /** * Translates the commands (control action or feedback) of a node to (intermediate) edges and adds them to the correct nodes. + * @param node The node of the commands. * @param commands The control actions or feedback of a node. * @param edgeType The type of the edge (control action or feedback). + * @param idToSNode The map of IDs to SNodes. * @param idCache The ID cache of the STPA model. + * @param addMissing Whether missing feedback should be added to the control structure. + * @param missingFeedback The missing feedbacks of the control structure. * @returns A list of edges representing the commands that should be added at the top level. */ export function translateCommandsToEdges( + source: Node, commands: VerticalEdge[], edgeType: EdgeType, idToSNode: Map, - idCache: IdCache + idCache: IdCache, + addMissing: boolean, + missingFeedback?: Map ): CSEdge[] { const edges: CSEdge[] = []; for (const edge of commands) { // create edge id - const source = edge.$container; const target = edge.target.ref; const edgeId = idCache.uniqueId( `${idCache.getId(source)}_${edge.comms[0].name}_${idCache.getId(target)}`, @@ -233,44 +257,79 @@ export function translateCommandsToEdges( const com = edge.comms[i]; label.push(com.label); } - // edges can be hierachy crossing so we must determine the common ancestor of source and target - const commonAncestor = getCommonAncestor(source, target); - // create the intermediate ports and edges - const ports = generateIntermediateCSEdges( - source, - target, - edgeId, - edgeType, - idToSNode, - idCache, - commonAncestor - ); - // add edge between the two ports in the common ancestor - const csEdge = createControlStructureEdge( - idCache.uniqueId(edgeId), - ports.sourcePort, - ports.targetPort, - label, - edgeType, - // if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge - target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, - idCache - ); - if (commonAncestor?.$type === "Graph") { - // if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned - edges.push(csEdge); - } else if (commonAncestor) { - // if the common ancestor is a node, the edge must be added to the children of the common ancestor - const snodeAncestor = idToSNode.get(idCache.getId(commonAncestor)!); - snodeAncestor?.children - ?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE) - ?.children?.push(csEdge); + createEdgeForCommand(source, target, edgeId, edgeType, label, idToSNode, idCache, edges); + } + } + + // add missing feedback edges + if (addMissing && missingFeedback) { + // add feedback edge to each node to which a feedback is missing + for (const target of missingFeedback.get(source?.name ?? "") ?? []) { + if (source && target) { + createEdgeForCommand( + source, + target, + idCache.uniqueId(`${source?.name}_missingFeedback_${target}`), + EdgeType.FEEDBACK, + // TODO: mark edge as missing to render it red + ["MISSING FEEDBACK"], + idToSNode, + idCache, + edges + ); } } } + return edges; } +/** + * Creates (intermediate) edges for the given {@code source} and {@code target} and adds them to the correct node. + * @param source The source of the edge. + * @param target The target of the edge. + * @param edgeId The ID of the edge. + * @param edgeType The type of the edge. + * @param label The label of the edge. + * @param idToSNode The map of IDs to SNodes. + * @param idCache The ID cache of the STPA model. + * @param edges The list of edges to add the created edges to. + */ +export function createEdgeForCommand( + source: Node, + target: Node, + edgeId: string, + edgeType: EdgeType, + label: string[], + idToSNode: Map, + idCache: IdCache, + edges: CSEdge[] +): void { + // edges can be hierachy crossing so we must determine the common ancestor of source and target + const commonAncestor = getCommonAncestor(source, target); + // create the intermediate ports and edges + const ports = generateIntermediateCSEdges(source, target, edgeId, edgeType, idToSNode, idCache, commonAncestor); + // add edge between the two ports in the common ancestor + const csEdge = createControlStructureEdge( + idCache.uniqueId(edgeId), + ports.sourcePort, + ports.targetPort, + label, + edgeType, + // if the common ancestor is the parent of the target we want an edge with an arrow otherwise an intermediate edge + target.$container === commonAncestor ? CS_EDGE_TYPE : CS_INTERMEDIATE_EDGE_TYPE, + idCache + ); + if (commonAncestor?.$type === "Graph") { + // if the common ancestor is the graph, the edge must be added at the top level and hence have to be returned + edges.push(csEdge); + } else if (commonAncestor) { + // if the common ancestor is a node, the edge must be added to the children of the common ancestor + const snodeAncestor = idToSNode.get(idCache.getId(commonAncestor)!); + snodeAncestor?.children?.find(node => node.type === CS_INVISIBLE_SUBCOMPONENT_TYPE)?.children?.push(csEdge); + } +} + /** * Translates the inputs or outputs of a node to edges. * @param io The inputs or outputs of a node. diff --git a/extension/src-language-server/stpa/diagram/diagram-generator.ts b/extension/src-language-server/stpa/diagram/diagram-generator.ts index d8d366e..ada77fa 100644 --- a/extension/src-language-server/stpa/diagram/diagram-generator.ts +++ b/extension/src-language-server/stpa/diagram/diagram-generator.ts @@ -161,7 +161,14 @@ export class StpaDiagramGenerator extends SnippetGraphGenerator { if (filteredModel.controlStructure) { // add control structure to roots children rootChildren.push( - createControlStructure(filteredModel.controlStructure, this.idToSNode, this.options, this.idCache) + createControlStructure( + filteredModel.controlStructure, + this.idToSNode, + this.options, + this.idCache, + this.options.getShowUnclosedFeedbackLoopsOption(), + this.services.validation.StpaValidator.missingFeedback + ) ); } // add relationship graph to roots children diff --git a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts index ffd731c..8f50276 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -41,6 +41,7 @@ const filterCategoryID = "filterCategory"; const showControlStructureID = "showControlStructure"; const showProcessModelsID = "showProcessModels"; +const showUnclosedFeedbackLoopsID = "showUnclosedFeedbackLoops"; const showRelationshipGraphID = "showRelationshipGraph"; /** @@ -312,6 +313,22 @@ const showLabelsOption: ValuedSynthesisOption = { currentValue: "automatic", }; +/** + * Boolean option to toggle the visualization of missing feedback in the control structure. + */ +const showUnclosedFeedbackLoopsOption: ValuedSynthesisOption = { + synthesisOption: { + id: showUnclosedFeedbackLoopsID, + name: "Missing Feedback Loops", + type: TransformationOptionType.CHECK, + initialValue: true, + currentValue: true, + values: [true, false], + category: filterCategory, + }, + currentValue: true, +}; + /** * Values for filtering the node labels. */ @@ -340,6 +357,7 @@ export class StpaSynthesisOptions extends SynthesisOptions { filteringOfUCAs, showControlStructureOption, showProcessModelsOption, + showUnclosedFeedbackLoopsOption, showRelationshipGraphOption, showSysConsOption, showRespsOption, @@ -511,6 +529,14 @@ export class StpaSynthesisOptions extends SynthesisOptions { return this.getOption(showSafetyConstraintsID)?.currentValue; } + setShowUnclosedFeedbackLoops(value: boolean): void { + this.setOption(showUnclosedFeedbackLoopsID, value); + } + + getShowUnclosedFeedbackLoopsOption(): boolean { + return this.getOption(showUnclosedFeedbackLoopsID)?.currentValue; + } + /** * Updates the filterUCAs option with the availabe cotrol actions. * @param values The currently avaiable control actions. diff --git a/extension/src-language-server/stpa/services/stpa-validator.ts b/extension/src-language-server/stpa/services/stpa-validator.ts index 7f21c7e..376c299 100644 --- a/extension/src-language-server/stpa/services/stpa-validator.ts +++ b/extension/src-language-server/stpa/services/stpa-validator.ts @@ -20,19 +20,19 @@ import { Position } from "vscode-languageserver-types"; import { Context, ControllerConstraint, + DCAContext, + DCARule, + Graph, Hazard, HazardList, Loss, Model, Node, - Responsibility, PastaAstType, + Responsibility, + Rule, SystemConstraint, isModel, - Graph, - Rule, - DCARule, - DCAContext, isRule, } from "../../generated/ast"; import { StpaServices } from "../stpa-module"; @@ -77,6 +77,11 @@ export class StpaValidator { checkForConflictingUCAs = true; + /** + * Map from node ID to a list of nodes to which a feedback is missing. + */ + missingFeedback: Map = new Map(); + /** * Executes validation checks for the whole model. * @param model The model to validate. @@ -434,6 +439,37 @@ export class StpaValidator { checkControlStructure(graph: Graph, accept: ValidationAcceptor): void { const nodes = [...graph.nodes, ...graph.nodes.map(node => this.getChildren(node)).flat(1)]; this.checkIDsAreUnique(nodes, accept); + this.checkForMissingFeedback(nodes, accept); + } + + /** + * Checks whether feedback is missing in the control structure and fills the missingFeedback map. + * @param nodes The nodes of the control structure. + * @param accept + */ + protected checkForMissingFeedback(nodes: Node[], accept: ValidationAcceptor): void { + // TODO: show missing feedback as warning in editor?? + this.missingFeedback.clear(); + for (const node of nodes) { + const nodeID = node.name; + // check for each action of the node whether feedback is missing + node.actions.forEach(action => { + const target = action.target.ref; + if (target) { + // check if target sents feedback back + const sentFeedback = target.feedbacks.find(feedback => feedback.target.$refText === nodeID); + if (!sentFeedback) { + // add the missing feedback to the map + const targetID = target.name; + if (!this.missingFeedback.has(targetID)) { + this.missingFeedback.set(targetID, [node]); + } else { + this.missingFeedback.get(targetID)?.push(node); + } + } + } + }); + } } /** @@ -559,7 +595,7 @@ export class StpaValidator { /** * Checks whether the model contains any TODOs. * @param model The model to check. - * @param accept + * @param accept */ protected checkForTODOs(model: Model, accept: ValidationAcceptor): void { model.losses.forEach(loss => {