diff --git a/extension/README.md b/extension/README.md index e0ffa06..bd41911 100644 --- a/extension/README.md +++ b/extension/README.md @@ -28,10 +28,11 @@ Alternatively the textual representation can be entered in the diagram snippets Instead of informal UCA definitions a context table may be used. This is done by using the section `Context-Table` instead of `UCAs`. A context table can then be generated automatically and shown alongside the diagram by selecting `Show Context Tables` in the editor context menu or the corresponding icon right above the editor. Clicking on a UCA in the context table highlights the corresponding node in the diagram and its definition in the editor. In the context table view a control action can be selected in order to inspect it. -### Completion Item +### Automation through Completion Item Partly information is repeated in STPA e.g. when defining a scenario for a UCA, the UCA itself is written down again. -To reduce the time effort, PASTA offers completion items, which generate text automatically based on the informations already stated in other components. +To reduce the time effort, PASTA offers completion items, which generate text automatically based on the informations already stated in other components. +To access completion items, press `ctrl` + `space`. The following completion items are provided: * create system component * create starting text for a plain text UCA @@ -51,7 +52,19 @@ The markdown file can easily be exported to a PDF file. ### Safe Behavioral Model Generation -In the context menu an option to automatically generate a safe behavioral model as an SCChart is provided. For that the defined UCAs are translated to LTL formulas, which are further used to create the SCChart. This guarantees that the identified UCAs cannot occur since the LTL formulas are respected except the ones for the UCA type too early. +In the context menu an option to automatically generate a safe behavioral model as an SCChart is provided. +For that the defined UCAs are translated to LTL formulas, which are further used to create the SCChart. +This guarantees that the identified UCAs cannot occur since the LTL formulas are respected except the ones for the UCA type **too early**. +For the generation you can define ranges for the process model variable values with standard range notation and the keywords `MIN` and `MAX`. +Example process model: +``` +processModel { + currentSpeed: [desiredSpeed=[desiredSpeed], lessDesiredSpeed = [MIN, desiredSpeed), greaterDesiredSpeed=(desiredSpeed, MAX]] +} +``` +In this case the controller has the process model variable `currentSpeed` which can take the values `desiredSpeed`, `lessDesiredSpeed`, and `greaterDesiredSpeed`. +For each of these values, the range it covers is defined. +`lessDesiredSpeed` covers every speed under the desired one, `greaterDesiredSpeed` every speed above the desired speed, and `desiredSpeed` only the desired speed. The SCChart language and an automatic visualization is provided by the two KIELER extensions [KLighD Diagrams](https://marketplace.visualstudio.com/items?itemName=kieler.klighd-vscode) and [KIELER VS Code](https://marketplace.visualstudio.com/items?itemName=kieler.keith-vscode). @@ -65,7 +78,12 @@ Furthermore, after an STPA is done, a corresponding Fault Tree can be generated ### STPA -To use the extension for an analysis, the file in which the analysis is done must have `.stpa` as its file ending. Each STPA aspect has its own section in the DSL. Components for each aspect are defined with an ID, a description, and a reference list. In order to define a new component, the prefix of the corresponding aspect must be stated, for example "L", and afterwards a string with the description. The numbering of the IDs is adjusted automatically. For Hazards and system-level constraints subcomponents can be defined. +To use the extension for an analysis, the file in which the analysis is done must have `.stpa` as its file ending. +Each STPA aspect has its own section in the DSL. Components for each aspect are defined with an ID, a description, and a reference list. +In order to define a new component, the prefix of the corresponding aspect must be stated, for example "L", and afterwards a string with the description. +The numbering of the IDs is adjusted automatically. +For Hazards and system-level constraints subcomponents can be defined. +For scenarios the causal factor can stated. In the control structure, system components can be stated, which can contain a process model, input, output, control actions, feedback, and further system components. The visualization of input and output edges is in an experimental state at the moment and will be improved in the future. @@ -137,7 +155,7 @@ ControllerConstraints C1 "ControlCentre must provide the Manual setting control action during VC malfunctioning and vessel too close to No Go Area" [UCA1] LossScenarios -Scenario1 for UCA1 "Abnormal vessel behavior occurs. Vessel comes too close to a No Go Area and ControlCentre does not manual set the parameters of the engine, causing the entering of a No Go Area." [H1] +Scenario1 for UCA1 "Abnormal vessel behavior occurs. Vessel comes too close to a No Go Area and ControlCentre does not manual set the parameters of the engine, causing the entering of a No Go Area." [H1] Scenario2 "Virtual Captain sends the Set parameters command upon coming too close to a No Go Area, but decceleration is not applied due to actuator failure." [H1] SafetyRequirements diff --git a/extension/package.json b/extension/package.json index 1daff82..5e4bbf9 100644 --- a/extension/package.json +++ b/extension/package.json @@ -142,6 +142,16 @@ "title": "✓ Set the safety requirements for UCA check", "category": "STPA Checks" }, + { + "command": "pasta.stpa.checks.setCheckMissingFeedback", + "title": "Set the missing feedback for system components check", + "category": "STPA Checks" + }, + { + "command": "pasta.stpa.checks.unsetCheckMissingFeedback", + "title": "✓ Set the missing feedback for system components check", + "category": "STPA Checks" + }, { "command": "pasta.IDs.undo", "title": "Executes the undo action", @@ -183,12 +193,12 @@ "category": "STPA Snippets" }, { - "command": "pasta.stpa.setIDGeneration", + "command": "pasta.stpa.setGenerateIDs", "title": "Automatic ID Generation", "category": "STPA ID Enforcement" }, { - "command": "pasta.stpa.unsetIDGeneration", + "command": "pasta.stpa.unsetGenerateIDs", "title": "✓ Automatic ID Generation", "category": "STPA ID Enforcement" } @@ -258,6 +268,14 @@ "command": "pasta.stpa.checks.unsetCheckSafetyRequirementsForUCAs", "when": "editorLangId == 'stpa' && checkSafetyRequirementsForUCAs == true" }, + { + "command": "pasta.stpa.checks.setCheckMissingFeedback", + "when": "editorLangId == 'stpa' && checkMissingFeedback == false" + }, + { + "command": "pasta.stpa.checks.unsetCheckMissingFeedback", + "when": "editorLangId == 'stpa' && checkMissingFeedback == true" + }, { "command": "pasta.stpa.SBM.generation", "when": "editorLangId == 'stpa'" @@ -275,12 +293,12 @@ "when": "editorLangId == 'stpa'" }, { - "command": "pasta.stpa.setIDGeneration", - "when": "editorLangId == 'stpa' && pasta.idGeneration == false" + "command": "pasta.stpa.setGenerateIDs", + "when": "editorLangId == 'stpa' && pasta.generateIDs == false" }, { - "command": "pasta.stpa.unsetIDGeneration", - "when": "editorLangId == 'stpa' && pasta.idGeneration == true" + "command": "pasta.stpa.unsetGenerateIDs", + "when": "editorLangId == 'stpa' && pasta.generateIDs == true" }, { "command": "pasta.fta.cutSets", @@ -333,13 +351,13 @@ "group": "stpa@4" }, { - "command": "pasta.stpa.setIDGeneration", - "when": "editorLangId == 'stpa' && pasta.idGeneration == false", + "command": "pasta.stpa.setGenerateIDs", + "when": "editorLangId == 'stpa' && pasta.generateIDs == false", "group": "stpa@1" }, { - "command": "pasta.stpa.unsetIDGeneration", - "when": "editorLangId == 'stpa' && pasta.idGeneration == true", + "command": "pasta.stpa.unsetGenerateIDs", + "when": "editorLangId == 'stpa' && pasta.generateIDs == true", "group": "stpa@1" }, { @@ -401,6 +419,18 @@ "title": "editorLangId == 'stpa'", "when": "editorLangId == 'stpa' && pasta.checkSafetyRequirementsForUCAs == true", "group": "checks@4" + }, + { + "command": "pasta.stpa.checks.setCheckMissingFeedback", + "title": "editorLangId == 'stpa'", + "when": "editorLangId == 'stpa' && pasta.checkMissingFeedback == false", + "group": "checks@5" + }, + { + "command": "pasta.stpa.checks.unsetCheckMissingFeedback", + "title": "editorLangId == 'stpa'", + "when": "editorLangId == 'stpa' && pasta.checkMissingFeedback == true", + "group": "checks@5" } ], "editor/title": [ diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts index 4a8f44b..1b94b2f 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,87 @@ 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 + let hasMissingFeedback = false; + for (const target of missingFeedback.get(source?.name ?? "") ?? []) { + if (source && target) { + hasMissingFeedback = true; + createEdgeForCommand( + source, + target, + idCache.uniqueId(`${source?.name}_missingFeedback_${target}`), + EdgeType.MISSING_FEEDBACK, + ["MISSING"], + idToSNode, + idCache, + edges + ); + } + } + // set flag for missing feedback to the source node + if (hasMissingFeedback) { + const sourceNode = idToSNode.get(idCache.getId(source) ?? ""); + if (sourceNode && sourceNode.type === CS_NODE_TYPE) { + (sourceNode as CSNode).hasMissingFeedback = true; } } } + 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-interfaces.ts b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts index 6408a5d..d636190 100644 --- a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts +++ b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts @@ -55,6 +55,7 @@ export interface PastaPort extends SPort { */ export interface CSNode extends SNode { level?: number; + hasMissingFeedback?: boolean; } /** diff --git a/extension/src-language-server/stpa/diagram/stpa-model.ts b/extension/src-language-server/stpa/diagram/stpa-model.ts index d541352..286688e 100644 --- a/extension/src-language-server/stpa/diagram/stpa-model.ts +++ b/extension/src-language-server/stpa/diagram/stpa-model.ts @@ -52,6 +52,7 @@ export enum STPAAspect { export enum EdgeType { CONTROL_ACTION, FEEDBACK, + MISSING_FEEDBACK, INPUT, OUTPUT, UNDEFINED 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 ddfda96..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"; /** @@ -291,9 +292,10 @@ const showLabelsOption: ValuedSynthesisOption = { id: showLabelsID, name: "Show Labels of", type: TransformationOptionType.DROPDOWN, - currentId: "losses", + currentId: "automatic", availableValues: [ { displayName: "All", id: "all" }, + { displayName: "Automatic", id: "automatic" }, { displayName: "Losses", id: "losses" }, { displayName: "Hazards", id: "hazards" }, { displayName: "System Constraints", id: "systemConstraints" }, @@ -302,14 +304,29 @@ const showLabelsOption: ValuedSynthesisOption = { { displayName: "Controller Constraints", id: "controllerConstraints" }, { displayName: "Scenarios", id: "scenarios" }, { displayName: "Safety Constraints", id: "safetyConstraints" }, - { displayName: "Automatic", id: "automatic" }, ], - initialValue: "losses", - currentValue: "losses", + initialValue: "automatic", + currentValue: "automatic", values: [], category: layoutCategory, } as DropDownOption, - currentValue: "losses", + 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, }; /** @@ -317,6 +334,7 @@ const showLabelsOption: ValuedSynthesisOption = { */ export enum showLabelsValue { ALL, + AUTOMATIC, LOSSES, HAZARDS, SYSTEM_CONSTRAINTS, @@ -325,7 +343,6 @@ export enum showLabelsValue { CONTROLLER_CONSTRAINTS, SCENARIOS, SAFETY_CONSTRAINTS, - AUTOMATIC, } export class StpaSynthesisOptions extends SynthesisOptions { @@ -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/message-handler.ts b/extension/src-language-server/stpa/message-handler.ts index 9e65cd7..ce6d51d 100644 --- a/extension/src-language-server/stpa/message-handler.ts +++ b/extension/src-language-server/stpa/message-handler.ts @@ -218,6 +218,7 @@ export function handleSTPAConfigReset(stpaServices: StpaServices): void { validator.checkConstraintsForUCAs = true; validator.checkScenariosForUCAs = true; validator.checkSafetyRequirementsForUCAs = true; + validator.checkMissingFeedback = true; // reset synthesis options stpaServices.options.SynthesisOptions.resetAll(); // reset ID generation option diff --git a/extension/src-language-server/stpa/services/stpa-validator.ts b/extension/src-language-server/stpa/services/stpa-validator.ts index 7f21c7e..8f0df35 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"; @@ -75,8 +75,16 @@ export class StpaValidator { /** Boolean option to toggle the check whether all UCAs are covered by safety requirements. */ checkSafetyRequirementsForUCAs = true; + /** Boolean option to toggle the check whether system components are missing feedback in the control structure. */ + checkMissingFeedback = true; + 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 +442,52 @@ 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 { + // fill the map with the missing feedback + 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); + } + } + } + }); + } + + // show warnings for all nodes that have missing feedback + if (this.checkMissingFeedback) { + nodes.forEach(node => { + const missingTargets = this.missingFeedback.get(node.name); + if (missingTargets) { + accept( + "warning", + "Feedback is missing to the following components: " + + missingTargets.map(target => target.label ?? target.name).join(), + { node: node, property: "name" } + ); + } + }); + } } /** @@ -559,7 +613,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 => { diff --git a/extension/src-language-server/synthesis-options.ts b/extension/src-language-server/synthesis-options.ts index 06291f2..e2d44a5 100644 --- a/extension/src-language-server/synthesis-options.ts +++ b/extension/src-language-server/synthesis-options.ts @@ -15,7 +15,13 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { RangeOption, SynthesisOption, TransformationOptionType, ValuedSynthesisOption } from "./options/option-models"; +import { + DropDownOption, + RangeOption, + SynthesisOption, + TransformationOptionType, + ValuedSynthesisOption, +} from "./options/option-models"; const labelManagementID = "labelManagement"; const labelShorteningWidthID = "labelShorteningWidth"; @@ -130,6 +136,9 @@ export class SynthesisOptions { option.currentValue = value; option.synthesisOption.currentValue = value; } + if (option?.synthesisOption.type === TransformationOptionType.DROPDOWN) { + (option.synthesisOption as DropDownOption).currentId = value; + } } /** diff --git a/extension/src-language-server/utils.ts b/extension/src-language-server/utils.ts index 3a8e6d5..16896b5 100644 --- a/extension/src-language-server/utils.ts +++ b/extension/src-language-server/utils.ts @@ -127,6 +127,9 @@ export function updateValidationChecks(options: Record, validator: case "checkSafetyRequirementsForUCAs": validator.checkSafetyRequirementsForUCAs = value; break; + case "checkMissingFeedback": + validator.checkMissingFeedback = value; + break; } }); } diff --git a/extension/src-webview/css/stpa-diagram.css b/extension/src-webview/css/stpa-diagram.css index 6512724..623b865 100644 --- a/extension/src-webview/css/stpa-diagram.css +++ b/extension/src-webview/css/stpa-diagram.css @@ -19,7 +19,27 @@ .stpa-node { stroke: black; -} +} + +/* red highlighting for visualization of missing feedback edges */ +.missing-feedback-label { + fill: red !important; +} + +.missing-feedback-node { + stroke: red !important; + stroke-width: 3 !important; +} + +.missing-edge { + stroke: red !important; + stroke-width: 3 !important; +} + +.missing-edge-arrow { + stroke: red !important; + fill: red !important; +} /* Feedback edges */ .feedback-edge { diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index c274d38..ec783bd 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -95,6 +95,7 @@ import { PolylineArrowEdgeView, PortView, STPAGraphView, + PastaLabelView, STPANodeView, } from "./stpa/stpa-views"; import { snippetModule } from './snippets/snippet-module'; @@ -119,8 +120,8 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = // configure the diagram elements const context = { bind, unbind, isBound, rebind }; - configureModelElement(context, "label", SLabel, SLabelView); - configureModelElement(context, "label:xref", SLabel, SLabelView); + configureModelElement(context, "label", SLabel, PastaLabelView); + configureModelElement(context, "label:xref", SLabel, PastaLabelView); configureModelElement(context, HEADER_LABEL_TYPE, SLabel, HeaderLabelView); configureModelElement(context, "html", HtmlRoot, HtmlRootView); configureModelElement(context, "pre-rendered", PreRenderedElement, PreRenderedView); diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 566e6f6..414f214 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -72,6 +72,7 @@ export class PastaPort extends SPort { */ export class CSNode extends SNode { level?: number; + hasMissingFeedback?: boolean; static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; } @@ -104,6 +105,7 @@ export enum STPAAspect { export enum EdgeType { CONTROL_ACTION, FEEDBACK, + MISSING_FEEDBACK, INPUT, OUTPUT, UNDEFINED, diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 3155441..f069406 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -24,7 +24,7 @@ import { ColorStyleOption, DifferentFormsOption, RenderOptionsRegistry } from '. import { SendModelRendererAction } from '../snippets/actions'; import { renderDiamond, renderHexagon, renderMirroredTriangle, renderOval, renderPentagon, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; import { collectAllChildren } from './helper-methods'; -import { CSEdge, CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; +import { CSEdge, CSNode, CS_EDGE_TYPE, CS_INTERMEDIATE_EDGE_TYPE, CS_NODE_TYPE, EdgeType, STPAAspect, STPAEdge, STPANode, STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa-model'; /** Determines if path/aspect highlighting is currently on. */ let highlighting: boolean; @@ -46,6 +46,8 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { const hidden = (edge.type === STPA_EDGE_TYPE || edge.type === STPA_INTERMEDIATE_EDGE_TYPE) && highlighting && !(edge as STPAEdge).highlight; // feedback edges in the control structure should be dashed const feedbackEdge = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.FEEDBACK; + // edges that represent missing edges should be highlighted + const missing = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.MISSING_FEEDBACK; const colorStyle = this.renderOptionsRegistry.getValue(ColorStyleOption); const printEdge = colorStyle === "black & white"; @@ -57,7 +59,7 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { aspect = (edge as STPAEdge).aspect % 2 === 0 || !lessColoredEdge ? (edge as STPAEdge).aspect : (edge as STPAEdge).aspect - 1; } return ; + class-feedback-edge={feedbackEdge} class-missing-edge={missing} class-greyed-out={hidden} aspect={aspect} d={path} />; } protected renderAdditionals(edge: SEdge, segments: Point[], context: RenderingContext): VNode[] { @@ -76,8 +78,11 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { if (edge.type === STPA_EDGE_TYPE || edge.type === STPA_INTERMEDIATE_EDGE_TYPE) { aspect = (edge as STPAEdge).aspect % 2 === 0 || !lessColoredEdge ? (edge as STPAEdge).aspect : (edge as STPAEdge).aspect - 1; } + // edges that represent missing edges should be highlighted + const missing = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.MISSING_FEEDBACK; + return [ - ]; @@ -208,8 +213,10 @@ export class CSNodeView extends RectangularNodeView { const colorStyle = this.renderOptionsRegistry.getValue(ColorStyleOption); const sprottyNode = colorStyle === "standard"; const printNode = !sprottyNode; + const missingFeedback = node.type === CS_NODE_TYPE && (node as CSNode).hasMissingFeedback; return - , context: RenderingContext): VNode | undefined { return {super.render(label, context)} - + ; + } +} + +@injectable() +export class PastaLabelView extends SLabelView { + render(label: Readonly, context: RenderingContext): VNode | undefined { + // label belongs to a node which may have missing feedback + const nodeMissingFeedback = label.parent.type === CS_NODE_TYPE && (label.parent as CSNode).hasMissingFeedback; + // label belongs to an edge which may be a missing feedback edge + const edgeMissingFeedback = (label.parent.type === CS_EDGE_TYPE || label.parent.type === CS_INTERMEDIATE_EDGE_TYPE) && (label.parent as CSEdge).edgeType === EdgeType.MISSING_FEEDBACK; + const missingFeedbackLabel = nodeMissingFeedback || edgeMissingFeedback; + + const vnode = super.render(label, context); + if (vnode?.data?.class) { + vnode.data.class['missing-feedback-label'] = missingFeedbackLabel ?? false; + } + + return vnode; } } diff --git a/extension/src/extension.ts b/extension/src/extension.ts index 4863150..27dd120 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -34,7 +34,28 @@ import { StorageService } from "./storage-service"; import { createFile, createOutputChannel, setStorageOption } from "./utils"; let languageClient: LanguageClient; -const validationGroupName = "validation"; + +/* The names of the groups for the storage options. */ +export const validationGroupName = "validation"; +export const idGenGroupName = "IDGeneration"; + +/* The contexts for the validation checks. */ +const validationContexts = [ + "checkResponsibilitiesForConstraints", + "checkConstraintsForUCAs", + "checkScenariosForUCAs", + "checkSafetyRequirementsForUCAs", + "checkMissingFeedback", +]; + +/* The contexts for the ID generation. */ +const idGenContexts = ["generateIDs"]; + +/* The command prefixes for toggling and contexts for the different storage groups. */ +const contextsAndPrefixForGroups: Map = new Map([ + [validationGroupName, {commandPrefix: ".stpa.checks", contexts: validationContexts}], + [idGenGroupName, {commandPrefix: ".stpa", contexts: idGenContexts}], +]); /** * All file endings of the languages that are supported by pasta. @@ -153,12 +174,13 @@ function registerPastaCommands( * Reset the contexts for storage options to the default values. */ function resetContextForStorageOptions(): void { - // set context for the validation checks depending on saved valued in storage + // set contexts for PASTA options depending on saved valued in storage vscode.commands.executeCommand("setContext", "pasta.checkResponsibilitiesForConstraints", true); vscode.commands.executeCommand("setContext", "pasta.checkConstraintsForUCAs", true); vscode.commands.executeCommand("setContext", "pasta.checkScenariosForUCAs", true); vscode.commands.executeCommand("setContext", "pasta.checkSafetyRequirementsForUCAs", true); - vscode.commands.executeCommand("setContext", "pasta.idGeneration", true); + vscode.commands.executeCommand("setContext", "pasta.checkMissingFeedback", true); + vscode.commands.executeCommand("setContext", "pasta.generateIDs", true); } /** @@ -186,140 +208,9 @@ function registerSTPACommands( ) ); - // set context for the validation checks depending on saved value in storage - const group = storage.getItem(validationGroupName); - vscode.commands.executeCommand( - "setContext", - "pasta.checkResponsibilitiesForConstraints", - group && group["checkResponsibilitiesForConstraints"] ? group["checkResponsibilitiesForConstraints"] : true - ); - vscode.commands.executeCommand( - "setContext", - "pasta.checkConstraintsForUCAs", - group && group["checkConstraintsForUCAs"] ? group["checkConstraintsForUCAs"] : true - ); - vscode.commands.executeCommand( - "setContext", - "pasta.checkScenariosForUCAs", - group && group["checkScenariosForUCAs"] ? group["checkScenariosForUCAs"] : true - ); - vscode.commands.executeCommand( - "setContext", - "pasta.checkSafetyRequirementsForUCAs", - group && group["checkSafetyRequirementsForUCAs"] ? group["checkSafetyRequirementsForUCAs"] : true - ); - // commands for toggling the provided validation checks - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.setCheckResponsibilitiesForConstraints", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkResponsibilitiesForConstraints", true); - setStorageOption( - validationGroupName, - "checkResponsibilitiesForConstraints", - true, - storage, - languageClient, - manager - ); - } - ) - ); - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.unsetCheckResponsibilitiesForConstraints", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkResponsibilitiesForConstraints", false); - setStorageOption( - validationGroupName, - "checkResponsibilitiesForConstraints", - false, - storage, - languageClient, - manager - ); - } - ) - ); - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.setCheckConstraintsForUCAs", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkConstraintsForUCAs", true); - setStorageOption( - validationGroupName, - "checkConstraintsForUCAs", - true, - storage, - languageClient, - manager - ); - } - ) - ); - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.unsetCheckConstraintsForUCAs", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkConstraintsForUCAs", false); - setStorageOption( - validationGroupName, - "checkConstraintsForUCAs", - false, - storage, - languageClient, - manager - ); - } - ) - ); - context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".stpa.checks.setCheckScenariosForUCAs", async () => { - vscode.commands.executeCommand("setContext", "pasta.checkScenariosForUCAs", true); - setStorageOption(validationGroupName, "checkScenariosForUCAs", true, storage, languageClient, manager); - }) - ); - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.unsetCheckScenariosForUCAs", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkScenariosForUCAs", false); - setStorageOption(validationGroupName, "checkScenariosForUCAs", false, storage, languageClient, manager); - } - ) - ); - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.setCheckSafetyRequirementsForUCAs", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkSafetyRequirementsForUCAs", true); - setStorageOption( - validationGroupName, - "checkSafetyRequirementsForUCAs", - true, - storage, - languageClient, - manager - ); - } - ) - ); - context.subscriptions.push( - vscode.commands.registerCommand( - options.extensionPrefix + ".stpa.checks.unsetCheckSafetyRequirementsForUCAs", - async () => { - vscode.commands.executeCommand("setContext", "pasta.checkSafetyRequirementsForUCAs", false); - setStorageOption( - validationGroupName, - "checkSafetyRequirementsForUCAs", - false, - storage, - languageClient, - manager - ); - } - ) - ); + setContextsBasedOnStorage(storage); + registerCommandsForStorageUpdate(manager, context, storage, options); + // needed to not activate ID generation on undo/redo context.subscriptions.push( vscode.commands.registerCommand(options.extensionPrefix + ".IDs.undo", async () => { @@ -382,28 +273,71 @@ function registerSTPACommands( return formulas; }) ); +} - // command for automating ID generation - const idGenGroup = "IDGeneration"; - vscode.commands.executeCommand( - "setContext", - "pasta.idGeneration", - storage.getItem(idGenGroup) && storage.getItem(idGenGroup)["generateIDs"] - ? storage.getItem(idGenGroup)["generateIDs"] - : true - ); - context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".stpa.setIDGeneration", async () => { - vscode.commands.executeCommand("setContext", "pasta.idGeneration", true); - setStorageOption(idGenGroup, "generateIDs", true, storage, languageClient, manager); - }) - ); - context.subscriptions.push( - vscode.commands.registerCommand(options.extensionPrefix + ".stpa.unsetIDGeneration", async () => { - vscode.commands.executeCommand("setContext", "pasta.idGeneration", false); - setStorageOption(idGenGroup, "generateIDs", false, storage, languageClient, manager); - }) - ); +/** + * Register the commands for setting and unsetting the contexts for the storage options. + * The command name must end with ".set" or ".unset" respectively and the name (camel-cased) of the context that should be set or unset. + * For the prefix of the command see {@code contextsAndPrefixForGroups}. + * The context name has to be the same as the key in the storage. + * @param manager The manager that handles the webview panels. + * @param context The context of the extension. + * @param storage The storage service for the extension. + * @param options The options for the commands. + */ +function registerCommandsForStorageUpdate( + manager: StpaLspVscodeExtension, + context: vscode.ExtensionContext, + storage: StorageService, + options: { extensionPrefix: string } +): void { + for (const groupName of contextsAndPrefixForGroups.keys()) { + const prefix = contextsAndPrefixForGroups.get(groupName)?.commandPrefix; + for (const contextOfGroup of contextsAndPrefixForGroups.get(groupName)?.contexts ?? []) { + context.subscriptions.push( + vscode.commands.registerCommand( + options.extensionPrefix + + prefix + ".set" + + contextOfGroup.charAt(0).toUpperCase() + + contextOfGroup.slice(1), + async () => { + vscode.commands.executeCommand("setContext", "pasta." + contextOfGroup, true); + setStorageOption(groupName, contextOfGroup, true, storage, languageClient, manager); + } + ) + ); + context.subscriptions.push( + vscode.commands.registerCommand( + options.extensionPrefix + + prefix + ".unset" + + contextOfGroup.charAt(0).toUpperCase() + + contextOfGroup.slice(1), + async () => { + vscode.commands.executeCommand("setContext", "pasta." + contextOfGroup, false); + setStorageOption(groupName, contextOfGroup, false, storage, languageClient, manager); + } + ) + ); + } + } +} + +/** + * Sets the context based on the storage for the custom contexts of PASTA. + * The context name has to be the same as the key in the storage. + * @param storage The storage service for the extension. + */ +function setContextsBasedOnStorage(storage: StorageService): void { + for (const groupName of contextsAndPrefixForGroups.keys()) { + const group = storage.getItem(groupName); + for (const contextOfGroup of contextsAndPrefixForGroups.get(groupName)?.contexts ?? []) { + vscode.commands.executeCommand( + "setContext", + "pasta." + contextOfGroup, + group && group[contextOfGroup] !== undefined ? group[contextOfGroup] : true + ); + } + } } /**