diff --git a/extension/src-language-server/diagram-server.ts b/extension/src-language-server/diagram-server.ts index fd9a5cf..3658963 100644 --- a/extension/src-language-server/diagram-server.ts +++ b/extension/src-language-server/diagram-server.ts @@ -15,7 +15,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { Action, DiagramServices, JsonMap, RequestAction, RequestModelAction, ResponseAction } from "sprotty-protocol"; +import { Action, CollapseExpandAction, CollapseExpandAllAction, DiagramServices, JsonMap, RequestAction, RequestModelAction, ResponseAction } from "sprotty-protocol"; import { Connection } from "vscode-languageserver"; import { FtaServices } from "./fta/fta-module.js"; import { SetSynthesisOptionsAction, UpdateOptionsAction } from "./options/actions.js"; @@ -53,6 +53,9 @@ import { import { StpaServices } from "./stpa/stpa-module.js"; import { SynthesisOptions } from "./synthesis-options.js"; +// matches id of a node to its expansion state. True means expanded, false and undefined means collapsed +export let expansionState = new Map(); + export class PastaDiagramServer extends SnippetDiagramServer { protected synthesisOptions: SynthesisOptions | undefined; protected stpaSnippets: StpaDiagramSnippets | undefined; @@ -103,10 +106,30 @@ export class PastaDiagramServer extends SnippetDiagramServer { return this.handleGenerateSVGDiagrams(action as GenerateSVGsAction); case UpdateDiagramAction.KIND: return this.updateView(this.state.options); + case CollapseExpandAction.KIND: + return this.collapseExpand(action as CollapseExpandAction); + case CollapseExpandAllAction.KIND: + // TODO: create buttons in sidepanel to send this action and implement the reaction to it + console.log("received collapse/expand all action"); } return super.handleAction(action); } + /** + * Collapses and expands the nodes with the given ids and updates the view. + * @param action The CollapseExpandAction that triggered this method. + * @returns + */ + protected collapseExpand(action: CollapseExpandAction): Promise { + for (const id of action.expandIds) { + expansionState.set(id, true); + } + for (const id of action.collapseIds) { + expansionState.set(id, false); + } + return this.updateView(this.state.options); + } + /** * Creates a snippet from a string. * @param text The text that should be inserted when clicking on the snippet. diff --git a/extension/src-language-server/layout-engine.ts b/extension/src-language-server/layout-engine.ts index 0d21d7a..2f74fa7 100644 --- a/extension/src-language-server/layout-engine.ts +++ b/extension/src-language-server/layout-engine.ts @@ -20,6 +20,7 @@ import { ElkLayoutEngine } from "sprotty-elk/lib/elk-layout.js"; import { Point, SEdge, SGraph, SModelIndex } from "sprotty-protocol"; import { FTAEdge } from "./fta/diagram/fta-interfaces.js"; import { FTA_EDGE_TYPE } from "./fta/diagram/fta-model.js"; +import { STPA_EDGE_TYPE, STPA_INTERMEDIATE_EDGE_TYPE } from './stpa/diagram/stpa-model.js'; export class LayoutEngine extends ElkLayoutEngine { layout(sgraph: SGraph, index?: SModelIndex): SGraph | Promise { @@ -52,10 +53,10 @@ export class LayoutEngine extends ElkLayoutEngine { }); } - /** Override method to save the junctionpoints in FTAEdges*/ + /** Override method to save junctionpoints*/ protected applyEdge(sedge: SEdge, elkEdge: ElkExtendedEdge, index: SModelIndex): void { const points: Point[] = []; - if (sedge.type === FTA_EDGE_TYPE) { + if (sedge.type === FTA_EDGE_TYPE || sedge.type === STPA_EDGE_TYPE || sedge.type === STPA_INTERMEDIATE_EDGE_TYPE) { (sedge as any as FTAEdge).junctionPoints = elkEdge.junctionPoints; } if (elkEdge.sections && elkEdge.sections.length > 0) { diff --git a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts index dc8072e..1c61afb 100644 --- a/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts +++ b/extension/src-language-server/stpa/diagram/diagram-controlStructure.ts @@ -18,6 +18,7 @@ import { AstNode } from "langium"; import { IdCache } from "langium-sprotty"; import { SModelElement, SNode } from "sprotty-protocol"; +import { expansionState } from "../../diagram-server.js"; import { Command, Graph, Node, Variable, VerticalEdge } from "../../generated/ast.js"; import { createControlStructureEdge, createDummyNode, createLabel, createPort } from "./diagram-elements.js"; import { CSEdge, CSNode, ParentNode } from "./stpa-interfaces.js"; @@ -94,8 +95,8 @@ export function createControlStructureNode( // add nodes representing the process model children.push(createProcessModelNodes(node.variables, idCache)); } - // add children of the control structure node - if (node.children?.length !== 0) { + // add children of the control structure node if the node is expanded + if (node.children?.length !== 0 && expansionState.get(node.name) === true) { // add invisible node to group the children in order to be able to lay them out separately from the process model node const invisibleNode = { type: CS_INVISIBLE_SUBCOMPONENT_TYPE, @@ -120,6 +121,8 @@ export function createControlStructureNode( id: nodeId, level: node.level, children: children, + hasChildren: node.children?.length !== 0, + expanded: expansionState.get(node.name) === true, layout: "stack", layoutOptions: { paddingTop: 10.0, @@ -154,6 +157,8 @@ export function createProcessModelNodes(variables: Variable[], idCache: IdCache< type: CS_NODE_TYPE, id: nodeId, children: children, + hasChildren: false, + expanded: expansionState.get(variable.name) === true, layout: "stack", layoutOptions: { paddingTop: 10.0, @@ -216,8 +221,12 @@ export function generateVerticalCSEdges( 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, addMissing, missingFeedback)); + + // add edges of the children of the node if the node is expanded + if (expansionState.get(node.name) === true) { + // create edges for children and add the ones that must be added at the top level + edges.push(...generateVerticalCSEdges(node.children, idToSNode, idCache, addMissing, missingFeedback)); + } } return edges; } @@ -520,7 +529,11 @@ export function generatePortsForCSHierarchy( const nodes: SNode[] = []; while (current && (!ancestor || current !== ancestor)) { const currentId = idCache.getId(current); - if (currentId) { + if (!currentId) { + // if the current node is collapsed, the ID is not set + // we may still want to draw the edge so far as possible to indicate the connection + current = current?.$container; + } else { const currentNode = idToSNode.get(currentId); if (currentNode) { // current node could have an invisible child that was skipped while going up the hierarchy because it does not exist in the AST diff --git a/extension/src-language-server/stpa/diagram/diagram-elements.ts b/extension/src-language-server/stpa/diagram/diagram-elements.ts index 82ca6eb..6c52e2f 100644 --- a/extension/src-language-server/stpa/diagram/diagram-elements.ts +++ b/extension/src-language-server/stpa/diagram/diagram-elements.ts @@ -188,6 +188,8 @@ export function createDummyNode(name: string, level: number | undefined, idCache type: DUMMY_NODE_TYPE, id: idCache.uniqueId("dummy" + name), layout: "stack", + hasChildren: false, + expanded: false, layoutOptions: { paddingTop: 10.0, paddingBottom: 10.0, diff --git a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts index 67f0ac4..900e490 100644 --- a/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts +++ b/extension/src-language-server/stpa/diagram/diagram-relationshipGraph.ts @@ -128,7 +128,13 @@ export function createRelationshipGraphChildren( .flat(1), ...sysCons .map(systemConstraint => - generateAspectWithEdges(systemConstraint, showSystemConstraintDescription, idToSNode, options, idCache) + generateAspectWithEdges( + systemConstraint, + showSystemConstraintDescription, + idToSNode, + options, + idCache + ) ) .flat(1), ]); @@ -140,13 +146,19 @@ export function createRelationshipGraphChildren( .flat(1), ...filteredModel.systemLevelConstraints ?.map(systemConstraint => - generateAspectWithEdges(systemConstraint, showSystemConstraintDescription, idToSNode, options, idCache) + generateAspectWithEdges( + systemConstraint, + showSystemConstraintDescription, + idToSNode, + options, + idCache + ) ) .flat(1), ...filteredModel.systemLevelConstraints ?.map(systemConstraint => systemConstraint.subComponents?.map(subsystemConstraint => - generateEdgesForSTPANode(subsystemConstraint, idToSNode, options, idCache) + generateEdgesForSTPANode(subsystemConstraint, undefined, idToSNode, options, idCache) ) ) .flat(2), @@ -158,7 +170,12 @@ export function createRelationshipGraphChildren( showLabels, labelManagement ); - const showUCAsDescription = showLabelOfAspect(STPAAspect.UCA, aspectsToShowDescriptions, showLabels, labelManagement); + const showUCAsDescription = showLabelOfAspect( + STPAAspect.UCA, + aspectsToShowDescriptions, + showLabels, + labelManagement + ); const showControllerConstraintDescription = showLabelOfAspect( STPAAspect.CONTROLLERCONSTRAINT, aspectsToShowDescriptions, @@ -300,7 +317,15 @@ export function generateAspectWithEdges( if ((isUCA(node) || isContext(node)) && node.$container.system.ref) { stpaNode.controlAction = node.$container.system.ref.name + "." + node.$container.action.ref?.name; } - const elements: SModelElement[] = generateEdgesForSTPANode(node, idToSNode, options, idCache); + + let nodePort: SModelElement | undefined; + if (options.getUseHyperEdges()) { + const nodeId = idCache.getId(node); + nodePort = createPort(idCache.uniqueId(nodeId + "_outPort"), PortSide.NORTH); + stpaNode?.children?.push(nodePort); + } + + const elements: SModelElement[] = generateEdgesForSTPANode(node, nodePort, idToSNode, options, idCache); elements.push(stpaNode); return elements; } @@ -366,11 +391,15 @@ export function generateSTPANode( /** * Generates the edges for {@code node}. * @param node STPA component for which the edges should be created. + * @param nodePort The port of the node. + * @param idToSNode The map of the generated IDs to their generated SNodes. + * @param options The synthesis options of the STPA model. * @param idCache The ID cache of the STPA model. * @returns Edges representing the references {@code node} contains. */ export function generateEdgesForSTPANode( node: AstNode, + nodePort: SModelElement | undefined, idToSNode: Map, options: StpaSynthesisOptions, idCache: IdCache @@ -379,8 +408,9 @@ export function generateEdgesForSTPANode( // for every reference an edge is created // if hierarchy option is false, edges from subcomponents to parents are created too const targets = getTargets(node, options.getHierarchy()); + for (const target of targets) { - const edge = generateSTPAEdge(node, target, "", idToSNode, idCache); + const edge = generateSTPAEdge(node, nodePort?.id, target, "", idToSNode, idCache); if (edge) { elements.push(edge); } @@ -391,13 +421,16 @@ export function generateEdgesForSTPANode( /** * Generates a single STPAEdge based on the given arguments. * @param source The source of the edge. + * @param sourcePortId The ID of the source port of the edge. * @param target The target of the edge. * @param label The label of the edge. - * @param param4 GeneratorContext of the STPA model. + * @param idToSNode The map of the generated IDs to their generated SNodes. + * @param idCache The ID cache of the STPA model. * @returns An STPAEdge. */ export function generateSTPAEdge( source: AstNode, + sourcePortId: string | undefined, target: AstNode, label: string, idToSNode: Map, @@ -421,6 +454,7 @@ export function generateSTPAEdge( target, source, sourceId, + sourcePortId, edgeId, children, idToSNode, @@ -428,25 +462,28 @@ export function generateSTPAEdge( ); } else { // otherwise it is sufficient to add ports for source and target - const portIds = createPortsForSTPAEdge( - sourceId, - PortSide.NORTH, - targetId, - PortSide.SOUTH, - edgeId, - idToSNode, - idCache - ); + let targetPortId: string | undefined; + if (sourcePortId) { + // if hyperedges are used, the source port is already given + const targetNode = idToSNode.get(targetId!); + targetPortId = idCache.uniqueId(edgeId + "_newTransition"); + targetNode?.children?.push(createPort(targetPortId, PortSide.SOUTH)); + } else { + const portIds = createPortsForSTPAEdge( + sourceId, + PortSide.NORTH, + targetId, + PortSide.SOUTH, + edgeId, + idToSNode, + idCache + ); + sourcePortId = portIds.sourcePortId; + targetPortId = portIds.targetPortId; + } // add edge between the two ports - return createSTPAEdge( - edgeId, - portIds.sourcePortId, - portIds.targetPortId, - children, - STPA_EDGE_TYPE, - getAspect(source) - ); + return createSTPAEdge(edgeId, sourcePortId, targetPortId, children, STPA_EDGE_TYPE, getAspect(source)); } } } @@ -458,6 +495,7 @@ export function generateSTPAEdge( * @param targetId The id of the target node. * @param targetSide The side of the target node the edge should be connected to. * @param edgeId The id of the edge. + * @param idToSNode The map of the generated IDs to their generated SNodes. * @param idCache The id cache of the STPA model. * @returns the ids of the source and target port the edge should be connected to. */ @@ -487,8 +525,10 @@ export function createPortsForSTPAEdge( * @param target The target of the edge. * @param source The source of the edge. * @param sourceId The ID of the source of the edge. + * @param sourcePortId The ID of the source port of the edge. * @param edgeId The ID of the original edge. * @param children The children of the original edge. + * @param idToSNode The map of the generated IDs to their generated SNodes. * @param idCache The ID cache of the STPA model. * @returns an STPAEdge to connect the {@code source} (or its top parent) with the top parent of the {@code target}. */ @@ -496,6 +536,7 @@ export function generateIntermediateIncomingSTPAEdges( target: AstNode, source: AstNode, sourceId: string, + sourcePortId: string | undefined, edgeId: string, children: SModelElement[], idToSNode: Map, @@ -533,10 +574,12 @@ export function generateIntermediateIncomingSTPAEdges( idCache ); } else { - // add port for source node - const sourceNode = idToSNode.get(sourceId); - const sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); - sourceNode?.children?.push(createPort(sourcePortId, PortSide.NORTH)); + if (!sourcePortId) { + // add port for source node + const sourceNode = idToSNode.get(sourceId); + sourcePortId = idCache.uniqueId(edgeId + "_newTransition"); + sourceNode?.children?.push(createPort(sourcePortId, PortSide.NORTH)); + } // add edge from source to top parent of the target return createSTPAEdge( @@ -556,6 +599,7 @@ export function generateIntermediateIncomingSTPAEdges( * @param edgeId The ID of the original edge. * @param children The children of the original edge. * @param targetPortId The ID of the target port. + * @param idToSNode The map of the generated IDs to their generated SNodes. * @param idCache The ID cache of the STPA model. * @returns the STPAEdge to connect the top parent of the {@code source} with the {@code targetPortId}. */ @@ -602,6 +646,7 @@ export function generateIntermediateOutgoingSTPAEdges( * @param current The current node. * @param edgeId The ID of the original edge for which the ports are created. * @param side The side of the ports. + * @param idToSNode The map of the generated IDs to their generated SNodes. * @param idCache The ID cache of the STPA model. * @returns the IDs of the created ports. */ diff --git a/extension/src-language-server/stpa/diagram/layout-config.ts b/extension/src-language-server/stpa/diagram/layout-config.ts index 77b26d3..d2f70be 100644 --- a/extension/src-language-server/stpa/diagram/layout-config.ts +++ b/extension/src-language-server/stpa/diagram/layout-config.ts @@ -77,7 +77,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { options["org.eclipse.elk.separateConnectedComponents"] = "false"; } if (csParent) { - options["org.eclipse.elk.layered.considerModelOrder.strategy"] = "PREFER_NODES"; + options["org.eclipse.elk.layered.considerModelOrder.strategy"] = "NODES_AND_EDGES"; options["org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder"] = "true"; options["org.eclipse.elk.layered.cycleBreaking.strategy"] = "MODEL_ORDER"; } @@ -122,7 +122,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { "org.eclipse.elk.partitioning.activate": "true", "org.eclipse.elk.direction": "DOWN", "org.eclipse.elk.portConstraints": "FIXED_SIDE", - "org.eclipse.elk.layered.considerModelOrder.strategy": "PREFER_NODES", + "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", "org.eclipse.elk.layered.cycleBreaking.strategy": "MODEL_ORDER", // nodes with many edges are streched @@ -191,7 +191,7 @@ export class StpaLayoutConfigurator extends DefaultLayoutConfigurator { // nodes with many edges are streched "org.eclipse.elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", "org.eclipse.elk.layered.nodePlacement.networkSimplex.nodeFlexibility.default": "NODE_SIZE", - "org.eclipse.elk.layered.considerModelOrder.strategy": "PREFER_NODES", + "org.eclipse.elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES", "org.eclipse.elk.layered.crossingMinimization.forceNodeModelOrder": "true", "org.eclipse.elk.layered.cycleBreaking.strategy": "MODEL_ORDER" }; diff --git a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts index 619731a..a56333f 100644 --- a/extension/src-language-server/stpa/diagram/stpa-interfaces.ts +++ b/extension/src-language-server/stpa/diagram/stpa-interfaces.ts @@ -15,7 +15,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SEdge, SNode, SPort } from "sprotty-protocol"; +import { Point, SEdge, SNode, SPort } from "sprotty-protocol"; import { EdgeType, PortSide, STPAAspect } from "./stpa-model.js"; export interface ParentNode extends SNode { @@ -41,6 +41,7 @@ export interface STPANode extends SNode { export interface STPAEdge extends SEdge { aspect: STPAAspect; highlight?: boolean; + junctionPoints?: Point[]; } /** Port representing a port in the STPA graph. */ @@ -56,6 +57,8 @@ export interface PastaPort extends SPort { export interface CSNode extends SNode { level?: number; hasMissingFeedback?: boolean; + hasChildren: boolean; + expanded: boolean; } /** 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 ec7e406..782cb4d 100644 --- a/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts +++ b/extension/src-language-server/stpa/diagram/stpa-synthesis-options.ts @@ -24,6 +24,7 @@ import { import { SynthesisOptions, layoutCategory } from "../../synthesis-options.js"; const hierarchyID = "hierarchy"; +const useHyperedgesID = "useHyperedges"; const groupingUCAsID = "groupingUCAs"; export const filteringUCAsID = "filteringUCAs"; @@ -329,6 +330,22 @@ const showUnclosedFeedbackLoopsOption: ValuedSynthesisOption = { currentValue: true, }; +/** + * Boolean option to toggle the visualization of loss scenarios. + */ +const useHyperedgesOption: ValuedSynthesisOption = { + synthesisOption: { + id: useHyperedgesID, + name: "Merge Edges", + type: TransformationOptionType.CHECK, + initialValue: true, + currentValue: true, + values: [true, false], + category: layoutCategory, + }, + currentValue: true, +}; + /** * Values for filtering the node labels. */ @@ -353,6 +370,7 @@ export class StpaSynthesisOptions extends SynthesisOptions { filterCategoryOption, showLabelsOption, groupingOfUCAs, + useHyperedgesOption, hierarchicalGraphOption, filteringOfUCAs, showControlStructureOption, @@ -537,6 +555,14 @@ export class StpaSynthesisOptions extends SynthesisOptions { return this.getOption(showUnclosedFeedbackLoopsID)?.currentValue; } + setUseHyperEdges(value: boolean): void { + this.setOption(useHyperedgesID, value); + } + + getUseHyperEdges(): boolean { + return this.getOption(useHyperedgesID)?.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/result-report/result-generator.ts b/extension/src-language-server/stpa/result-report/result-generator.ts index cc28fb6..e78efc3 100644 --- a/extension/src-language-server/stpa/result-report/result-generator.ts +++ b/extension/src-language-server/stpa/result-report/result-generator.ts @@ -43,9 +43,25 @@ export async function createResultData(uri: string, shared: LangiumSprottyShared // get the current model const model = await getModel(uri, shared) as Model; + // TODO: make more generic (see goals, assumptions, losses) + + // goals + const resultGoals: { id: string; description: string }[] = []; + model.goals?.forEach((component) => { + resultGoals.push({ id: component.name, description: component.description }); + }); + result.goals = resultGoals; + + // assumptions + const resultAssumptions: { id: string; description: string }[] = []; + model.assumptions?.forEach((component) => { + resultAssumptions.push({ id: component.name, description: component.description }); + }); + result.assumptions = resultAssumptions; + // losses const resultLosses: { id: string; description: string }[] = []; - model.losses.forEach((component) => { + model.losses?.forEach((component) => { resultLosses.push({ id: component.name, description: component.description }); }); result.losses = resultLosses; @@ -55,7 +71,7 @@ export async function createResultData(uri: string, shared: LangiumSprottyShared result.systemLevelConstraints = createHazardOrSystemConstraintComponents(model.systemLevelConstraints); // controller constraints sorted by control action - model.controllerConstraints.forEach((component) => { + model.controllerConstraints?.forEach((component) => { const resultComponent = createSingleComponent(component); const controlAction = createControlActionText(component.refs[0].ref?.$container); if (result.controllerConstraints[controlAction] === undefined) { @@ -70,23 +86,23 @@ export async function createResultData(uri: string, shared: LangiumSprottyShared result.safetyConstraints = createResultComponents(model.safetyCons); // responsibilities - model.responsibilities.forEach((component) => { + model.responsibilities?.forEach((component) => { const responsibilities = createResultComponents(component.responsiblitiesForOneSystem); // responsibilities are grouped by their system component result.responsibilities[component.system.$refText] = responsibilities; }); // loss scenarios - model.scenarios.forEach((component) => { + model.scenarios?.forEach((component) => { createScenarioResult(component, result); }); //UCAs - model.allUCAs.forEach((component) => { + model.allUCAs?.forEach((component) => { const ucaResult = createUCAResult(component); result.ucas[ucaResult.controlAction] = ucaResult.ucas; }); - model.rules.forEach((component) => { + model.rules?.forEach((component) => { addRuleUCA(component, result); }); diff --git a/extension/src-language-server/stpa/stpa.langium b/extension/src-language-server/stpa/stpa.langium index abbab18..d77d98f 100644 --- a/extension/src-language-server/stpa/stpa.langium +++ b/extension/src-language-server/stpa/stpa.langium @@ -18,6 +18,8 @@ grammar Stpa entry Model: + ('Goals' goals+=Goal*)? + ('Assumptions' assumptions+=Assumption*)? ('Losses' losses+=Loss*)? ('Hazards' hazards+=Hazard*)? ('SystemConstraints' systemLevelConstraints+=SystemConstraint*)? @@ -29,6 +31,12 @@ entry Model: ('LossScenarios' scenarios+=LossScenario*)? ('SafetyRequirements' safetyCons+=SafetyConstraint*)?; +Goal: + name=ID description=STRING; + +Assumption: + name=ID description=STRING; + Rule: name=ID '{' 'controlAction:' system=[Node] '.' action=[Command] diff --git a/extension/src-language-server/stpa/utils.ts b/extension/src-language-server/stpa/utils.ts index b60090e..222d7b8 100644 --- a/extension/src-language-server/stpa/utils.ts +++ b/extension/src-language-server/stpa/utils.ts @@ -124,6 +124,8 @@ export function collectElementsWithSubComps(topElements: (Hazard | SystemConstra export class StpaResult { title: string; + goals: StpaComponent[] = []; + assumptions: StpaComponent[] = []; losses: StpaComponent[] = []; hazards: StpaComponent[] = []; systemLevelConstraints: StpaComponent[] = []; diff --git a/extension/src-webview/css/diagram.css b/extension/src-webview/css/diagram.css index 06e8fac..9b45eb2 100644 --- a/extension/src-webview/css/diagram.css +++ b/extension/src-webview/css/diagram.css @@ -22,6 +22,14 @@ @import "./fta-diagram.css"; @import "./context-menu.css"; +.collapse-expand-triangle { + fill: darkgrey; +} + +.collapse-expand-sign { + stroke: white; +} + .header { text-decoration-line: underline; font-weight: bold; @@ -33,8 +41,8 @@ /* sprotty and black/white colors */ .vscode-high-contrast .print-node { - fill: black; stroke: white; + fill-opacity: 0; } .vscode-high-contrast .sprotty-node { @@ -43,8 +51,8 @@ } .vscode-dark .print-node { - fill: var(--dark-node); stroke: var(--vscode-editor-foreground); + fill-opacity: 0; } .vscode-dark .sprotty-node { @@ -53,8 +61,8 @@ } .vscode-light .print-node { - fill: var(--light-node); stroke: black; + fill-opacity: 0; } .vscode-light .sprotty-node { diff --git a/extension/src-webview/css/stpa-diagram.css b/extension/src-webview/css/stpa-diagram.css index 623b865..7c02fbb 100644 --- a/extension/src-webview/css/stpa-diagram.css +++ b/extension/src-webview/css/stpa-diagram.css @@ -42,10 +42,28 @@ } /* Feedback edges */ -.feedback-edge { +.feedback-dotted { stroke-dasharray: 5, 5; } +.vscode-light .feedback-grey { + stroke: var(--stpa-feedback-grey-light); +} + +.vscode-light .feedback-grey-arrow { + stroke: var(--stpa-feedback-grey-light) !important; + fill: var(--stpa-feedback-grey-light) !important; +} + +.vscode-dark .feedback-grey { + stroke: var(--stpa-feedback-grey-dark); +} + +.vscode-dark .feedback-grey-arrow { + stroke: var(--stpa-feedback-grey-dark) !important; + fill: var(--stpa-feedback-grey-dark) !important; +} + /* High contrast theme */ .vscode-high-contrast .stpa-node[aspect="0"], .vscode-high-contrast .stpa-edge-arrow[aspect="0"] { diff --git a/extension/src-webview/css/theme.css b/extension/src-webview/css/theme.css index 1caacb5..86107d0 100644 --- a/extension/src-webview/css/theme.css +++ b/extension/src-webview/css/theme.css @@ -60,5 +60,8 @@ --stpa-scenario-light: #cccc00; --stpa-safety-requirement-light: #8fbcbc; + --stpa-feedback-grey-light: darkgrey; + --stpa-feedback-grey-dark: slategrey; + /* background: #feffe8 */ } diff --git a/extension/src-webview/di.config.ts b/extension/src-webview/di.config.ts index 7685d9b..b041cbd 100644 --- a/extension/src-webview/di.config.ts +++ b/extension/src-webview/di.config.ts @@ -83,6 +83,7 @@ import { PASTA_LABEL_TYPE, PORT_TYPE, PROCESS_MODEL_PARENT_NODE_TYPE, + ParentNode, PastaPort, STPAEdge, STPANode, @@ -136,7 +137,7 @@ const pastaDiagramModule = new ContainerModule((bind, unbind, isBound, rebind) = configureModelElement(context, DUMMY_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, CS_NODE_TYPE, CSNode, CSNodeView); configureModelElement(context, STPA_NODE_TYPE, STPANode, STPANodeView); - configureModelElement(context, PARENT_TYPE, SNodeImpl, CSNodeView); + configureModelElement(context, PARENT_TYPE, ParentNode, CSNodeView); configureModelElement(context, STPA_EDGE_TYPE, STPAEdge, PolylineArrowEdgeView); configureModelElement(context, STPA_INTERMEDIATE_EDGE_TYPE, STPAEdge, IntermediateEdgeView); configureModelElement(context, CS_INTERMEDIATE_EDGE_TYPE, CSEdge, IntermediateEdgeView); diff --git a/extension/src-webview/diagram-server.ts b/extension/src-webview/diagram-server.ts index 6d62a35..94e79e6 100644 --- a/extension/src-webview/diagram-server.ts +++ b/extension/src-webview/diagram-server.ts @@ -17,7 +17,7 @@ import { injectable } from "inversify"; import { ActionHandlerRegistry } from "sprotty"; -import { Action, ActionMessage } from "sprotty-protocol"; +import { Action, ActionMessage, CollapseExpandAction } from "sprotty-protocol"; import { VscodeLspEditDiagramServer } from "sprotty-vscode-webview/lib/lsp/editing"; import { SvgAction } from "./actions"; @@ -37,6 +37,7 @@ export class PastaDiagramServer extends VscodeLspEditDiagramServer { handleLocally(action: Action): boolean { switch (action.kind) { case SvgAction.KIND: + case CollapseExpandAction.KIND: this.handleSvgAction(action as SvgAction); } return super.handleLocally(action); @@ -47,7 +48,7 @@ export class PastaDiagramServer extends VscodeLspEditDiagramServer { * @param action The SVGAction. * @returns */ - handleSvgAction(action: SvgAction): boolean { + handleSvgAction(action: SvgAction | CollapseExpandAction): boolean { this.forwardToServer(action); return false; } diff --git a/extension/src-webview/options/render-options-registry.ts b/extension/src-webview/options/render-options-registry.ts index e8ef567..78beab7 100644 --- a/extension/src-webview/options/render-options-registry.ts +++ b/extension/src-webview/options/render-options-registry.ts @@ -53,6 +53,23 @@ export class DifferentFormsOption implements RenderOption { currentValue = false; } +export const lightGreyFeedback = "light grey"; +export const dottedFeedback = "dotted"; + +/** + * Different options for the color style of feedback edges. + */ +export class FeedbackStyleOption implements ChoiceRenderOption { + static readonly ID: string = "feedbackStyle"; + static readonly NAME: string = "Feedback Style"; + readonly id: string = FeedbackStyleOption.ID; + readonly name: string = FeedbackStyleOption.NAME; + readonly type: TransformationOptionType = TransformationOptionType.CHOICE; + readonly availableValues: string[] = ["normal", dottedFeedback, lightGreyFeedback]; + readonly initialValue: string = dottedFeedback; + currentValue = dottedFeedback; +} + export interface RenderOptionType { readonly ID: string; readonly NAME: string; @@ -75,6 +92,7 @@ export class RenderOptionsRegistry extends Registry { // Add available render options to this registry this.register(DifferentFormsOption); this.register(ColorStyleOption); + this.register(FeedbackStyleOption); } @postConstruct() diff --git a/extension/src-webview/stpa/stpa-model.ts b/extension/src-webview/stpa/stpa-model.ts index 8767088..5954980 100644 --- a/extension/src-webview/stpa/stpa-model.ts +++ b/extension/src-webview/stpa/stpa-model.ts @@ -15,8 +15,8 @@ * SPDX-License-Identifier: EPL-2.0 */ -import { SEdgeImpl, SLabelImpl, SNodeImpl, SPortImpl, alignFeature, boundsFeature, connectableFeature, fadeFeature, layoutContainerFeature, layoutableChildFeature, selectFeature } from "sprotty"; -import { EdgePlacement } from "sprotty-protocol"; +import { SEdgeImpl, SLabelImpl, SNodeImpl, SPortImpl, alignFeature, boundsFeature, connectableFeature, fadeFeature, layoutContainerFeature, layoutableChildFeature, selectFeature, expandFeature } from "sprotty"; +import { EdgePlacement, Point } from "sprotty-protocol"; // The types of diagram elements export const STPA_NODE_TYPE = 'node:stpa'; @@ -37,6 +37,7 @@ export const EDGE_LABEL_TYPE = 'label:xref'; export class ParentNode extends SNodeImpl { modelOrder: boolean; + static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; } /** @@ -60,6 +61,7 @@ export class STPANode extends SNodeImpl { export class STPAEdge extends SEdgeImpl { aspect: STPAAspect = STPAAspect.UNDEFINED; highlight?: boolean; + junctionPoints?: Point[]; static readonly DEFAULT_FEATURES = [fadeFeature]; } @@ -76,7 +78,9 @@ export class PastaPort extends SPortImpl { export class CSNode extends SNodeImpl { level?: number; hasMissingFeedback?: boolean; - static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature]; + hasChildren: boolean; + expanded: boolean; + static readonly DEFAULT_FEATURES = [connectableFeature, selectFeature, layoutContainerFeature, fadeFeature, expandFeature]; } /** diff --git a/extension/src-webview/stpa/stpa-mouselistener.ts b/extension/src-webview/stpa/stpa-mouselistener.ts index d4ba757..91b1dc7 100644 --- a/extension/src-webview/stpa/stpa-mouselistener.ts +++ b/extension/src-webview/stpa/stpa-mouselistener.ts @@ -1,10 +1,9 @@ -import { MouseListener, SLabelImpl, SModelElementImpl } from "sprotty"; -import { Action } from "sprotty-protocol"; +import { isExpandable, MouseListener, SLabelImpl, SModelElementImpl } from "sprotty"; +import { Action, CollapseExpandAction } from "sprotty-protocol"; import { flagConnectedElements, flagSameAspect } from "./helper-methods"; -import { STPAEdge, STPANode, STPA_NODE_TYPE } from "./stpa-model"; +import { CS_NODE_TYPE, STPA_NODE_TYPE, STPAEdge, STPANode } from "./stpa-model"; export class StpaMouseListener extends MouseListener { - protected flaggedElements: (STPANode | STPAEdge)[] = []; mouseDown(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { @@ -25,6 +24,21 @@ export class StpaMouseListener extends MouseListener { return []; } + doubleClick(target: SModelElementImpl, event: MouseEvent): (Action | Promise)[] { + // when a label is selected, we are interested in its parent node + target = target instanceof SLabelImpl ? target.parent : target; + // if the selected node is expandable, the node should be expanded or collapsed + if (target.type === CS_NODE_TYPE && isExpandable(target)) { + return [ + CollapseExpandAction.create({ + expandIds: target.expanded ? [] : [target.id], + collapseIds: target.expanded ? [target.id] : [], + }), + ]; + } + return []; + } + /** * Resets the highlight attribute of the highlighted nodes. */ @@ -34,5 +48,4 @@ export class StpaMouseListener extends MouseListener { } this.flaggedElements = []; } - -} \ No newline at end of file +} diff --git a/extension/src-webview/stpa/stpa-views.tsx b/extension/src-webview/stpa/stpa-views.tsx index 105d24a..c263b15 100644 --- a/extension/src-webview/stpa/stpa-views.tsx +++ b/extension/src-webview/stpa/stpa-views.tsx @@ -21,9 +21,9 @@ import { VNode } from 'snabbdom'; import { IActionDispatcher, IView, IViewArgs, ModelRenderer, PolylineEdgeView, RectangularNodeView, RenderingContext, SEdgeImpl, SGraphImpl, SGraphView, SLabelImpl, SLabelView, SNodeImpl, SPortImpl, TYPES, svg } from 'sprotty'; import { Point, toDegrees } from "sprotty-protocol"; import { DISymbol } from '../di.symbols'; -import { ColorStyleOption, DifferentFormsOption, RenderOptionsRegistry } from '../options/render-options-registry'; +import { ColorStyleOption, DifferentFormsOption, FeedbackStyleOption, RenderOptionsRegistry, dottedFeedback, lightGreyFeedback } from '../options/render-options-registry'; import { SendModelRendererAction } from '../snippets/actions'; -import { renderDiamond, renderHexagon, renderMirroredTriangle, renderOval, renderPentagon, renderPort, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; +import { renderCollapseIcon, renderDiamond, renderEllipse, renderExpandIcon, renderHexagon, renderMirroredTriangle, renderOval, renderPentagon, renderRectangle, renderRoundedRectangle, renderTrapez, renderTriangle } from '../views-rendering'; import { collectAllChildren } from './helper-methods'; 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'; @@ -35,17 +35,46 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { @inject(DISymbol.RenderOptionsRegistry) renderOptionsRegistry: RenderOptionsRegistry; + /** + * Shifts the edge point to adjust the start/end point of an edge. + * @param p The point to shift. + * @param compareStart Start compare point to determine the direction of the edge. + * @param compareEnd End compare point to determine the direction of the edge. + * @param shift The amount to shift the point. + * @returns a new shifted point. + */ + protected shiftEdgePoint(p: Point, compareStart: Point, compareEnd: Point, shift: number): Point { + // for some reason sometimes the x values are apart by 0.5 although they should be the same, so this is a workaround to fix this + const x = compareEnd.x - compareStart.x === 0.5 ? compareEnd.x : (compareEnd.x - compareStart.x === -0.5 ? compareStart.x : p.x); + // shift the y value of the point to adjust start/end point of an edge + if (compareStart.y < compareEnd.y) { + // edge goes down + return { x: x, y: p.y - shift }; + } else { + // edge goes up + return { x: x, y: p.y + shift }; + } + } + protected renderLine(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode { const firstPoint = segments[0]; - let path = `M ${firstPoint.x},${firstPoint.y}`; + // adjust first point to not have a gap between node and edge + const start = this.shiftEdgePoint(firstPoint, firstPoint, segments[1], 1); + let path = `M ${start.x},${start.y}`; for (let i = 1; i < segments.length; i++) { const p = segments[i]; - path += ` L ${p.x},${p.y}`; + // adjust the last point if it is not an intermediate edge in order to draw the arrow correctly (not reaching into the target node) + if ((edge.type === CS_EDGE_TYPE || edge.type === STPA_EDGE_TYPE) && i === segments.length - 1) { + const lastPoint = this.shiftEdgePoint(p, segments[segments.length - 2], p, 2); + path += ` L ${lastPoint.x},${lastPoint.y}`; + } else { + path += ` L ${p.x},${p.y}`; + } } // if an STPANode is selected, the components not connected to it should fade out 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 + // feedback edges in the control structure are possibly styled differently 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; @@ -56,19 +85,34 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { const lessColoredEdge = colorStyle === "fewer colors"; // coloring of the edge depends on the aspect let aspect: number = -1; + // renderings for all junction points + let junctionPointRenderings: VNode[] = []; 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; + junctionPointRenderings = (edge as STPAEdge).junctionPoints?.map(junctionPoint => + renderEllipse(junctionPoint.x, junctionPoint.y, 4, 4, 1) + ) ?? []; } - return ; + const feedbackStyle = this.renderOptionsRegistry.getValue(FeedbackStyleOption); + const dotted = feedbackStyle === dottedFeedback; + const greyFeedback = feedbackStyle === lightGreyFeedback; + return + + {...(junctionPointRenderings ?? [])} + ; } protected renderAdditionals(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode[] { // if an STPANode is selected, the components not connected to it should fade out const hidden = edge.type === STPA_EDGE_TYPE && highlighting && !(edge as STPAEdge).highlight; - const p1 = segments[segments.length - 2]; - const p2 = segments[segments.length - 1]; + const forelastSegment = segments[segments.length - 2]; + const lastSegment = segments[segments.length - 1]; + // determine the last point to draw the arrow correctly (not reaching into the target node) + const lastPoint = this.shiftEdgePoint(lastSegment, forelastSegment, lastSegment, 1); + const endpoint = `${lastPoint.x} ${lastPoint.y}`; + const colorStyle = this.renderOptionsRegistry.getValue(ColorStyleOption); const printEdge = colorStyle === "black & white"; @@ -82,10 +126,15 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { // 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; + // feedback edges in the control structure are possibly styled differently + const feedbackEdge = (edge.type === CS_EDGE_TYPE || edge.type === CS_INTERMEDIATE_EDGE_TYPE) && (edge as CSEdge).edgeType === EdgeType.FEEDBACK; + const feedbackStyle = this.renderOptionsRegistry.getValue(FeedbackStyleOption); + const greyFeedback = feedbackStyle === lightGreyFeedback; return [ + transform={`rotate(${this.angle(lastPoint, forelastSegment)} ${endpoint}) translate(${endpoint})`} /> ]; } @@ -97,25 +146,13 @@ export class PolylineArrowEdgeView extends PolylineEdgeView { @injectable() export class IntermediateEdgeView extends PolylineArrowEdgeView { - render(edge: Readonly, context: RenderingContext, args?: IViewArgs): VNode | undefined { - const route = this.edgeRouterRegistry.route(edge, args); - if (route.length === 0) { - return this.renderDanglingEdge("Cannot compute route", edge, context); - } - if (!this.isVisible(edge, route, context)) { - if (edge.children.length === 0) { - return undefined; - } - // The children of an edge are not necessarily inside the bounding box of the route, - // so we need to render a group to ensure the children have a chance to be rendered. - return {context.renderChildren(edge, { route })}; - } - - // intermediate edge do not have an arrow - return - {this.renderLine(edge, route, context)} - {context.renderChildren(edge, { route })} - ; + protected renderAdditionals(edge: SEdgeImpl, segments: Point[], context: RenderingContext): VNode[] { + // const p = segments[segments.length - 1]; + // return [ + // + // ]; + return []; } } @@ -215,15 +252,26 @@ export class CSNodeView extends RectangularNodeView { const sprottyNode = colorStyle === "standard"; const printNode = !sprottyNode; const missingFeedback = node.type === CS_NODE_TYPE && (node as CSNode).hasMissingFeedback; - return - - {context.renderChildren(node)} - ; + > ; + if (node.type === CS_NODE_TYPE && (node as CSNode).hasChildren) { + // render the expand/collapse icon indicating that the node can be expanded + const icon = (node as CSNode).expanded ? renderCollapseIcon() : renderExpandIcon(); + return + {icon} + {rectangle} + {context.renderChildren(node)} + ; + } else { + return + {rectangle} + {context.renderChildren(node)} + ; + } } } @@ -266,6 +314,13 @@ export class PortView implements IView { } } +export function renderPort(x:number, y: number, width: number, height: number): VNode { + return ; +} + @injectable() export class HeaderLabelView extends SLabelView { render(label: Readonly, context: RenderingContext): VNode | undefined { @@ -293,3 +348,4 @@ export class EdgeLabelView extends SLabelView { return vnode; } } + diff --git a/extension/src-webview/views-rendering.tsx b/extension/src-webview/views-rendering.tsx index 51da7e6..82b374e 100644 --- a/extension/src-webview/views-rendering.tsx +++ b/extension/src-webview/views-rendering.tsx @@ -311,4 +311,59 @@ export function renderInhibitGate(node: SNodeImpl): VNode { return ; +} + +/** + * Creates the icon for collapsing a node. + * @returns + */ +export function renderCollapseIcon(): VNode { + return + {renderCollapseExpandTriangle()} + {renderCollapseSign()} + ; +} + +/** + * Creates the triangle for the expand and collapse icon. + * @returns + */ +export function renderCollapseExpandTriangle(): VNode { + const path = `M 0 0 L 0 15 L 15 0 Z`; + return ; +} + +/** + * Creates the minus-sign for the collapse icon. + * @returns + */ +export function renderCollapseSign(): VNode { + const path = `M 3 5 L 7 5`; + return ; +} + +/** + * Creates the icon for expanding a node. + * @returns + */ +export function renderExpandIcon(): VNode { + return + {renderCollapseExpandTriangle()} + {renderExpandSign()} + ; +} + +/** + * Creates the plus-sign for the expand icon. + * @returns + */ +export function renderExpandSign(): VNode { + const path = `M 3 5 L 7 5 M 5 3 L 5 7`; + return ; } \ No newline at end of file diff --git a/extension/src/report/md-export.ts b/extension/src/report/md-export.ts index d1a73f9..045a84d 100644 --- a/extension/src/report/md-export.ts +++ b/extension/src/report/md-export.ts @@ -82,7 +82,11 @@ export async function createSTPAResultMarkdownFile(data: StpaResult, extension: function createSTPAResultMarkdownText(data: StpaResult, diagramSizes: Record): string { let markdown = `# STPA Report for ${data.title}\n\n`; // table of contents - markdown += createTOC(); + markdown += createTOC() + "\n"; + // goals + markdown += stpaAspectToMarkdown(Headers.Goal, data.goals) + "\n"; + // assumptions + markdown += stpaAspectToMarkdown(Headers.Assumption, data.assumptions) + "\n"; // losses markdown += stpaAspectToMarkdown(Headers.Loss, data.losses) + "\n"; // hazards @@ -407,25 +411,27 @@ function addCopyRight(): string { function createTOC(): string { //TODO: use regex for the whitespace let markdown = "## Table of Contents\n\n"; - markdown += `1. [${Headers.Loss}](#${Headers.Loss.toLowerCase()})\n`; - markdown += `2. [${Headers.Hazard}](#${Headers.Hazard.toLowerCase()})\n`; - markdown += `3. [${Headers.SystemLevelConstraint}](#${Headers.SystemLevelConstraint.toLowerCase().replace( + markdown += `1. [${Headers.Goal}](#${Headers.Goal.toLowerCase()})\n`; + markdown += `2. [${Headers.Assumption}](#${Headers.Assumption.toLowerCase()})\n`; + markdown += `3. [${Headers.Loss}](#${Headers.Loss.toLowerCase()})\n`; + markdown += `4. [${Headers.Hazard}](#${Headers.Hazard.toLowerCase()})\n`; + markdown += `5. [${Headers.SystemLevelConstraint}](#${Headers.SystemLevelConstraint.toLowerCase().replace( " ", "-" )})\n`; - markdown += `4. [${Headers.ControlStructure}](#${Headers.ControlStructure.toLowerCase().replace(" ", "-")})\n`; - markdown += `5. [${Headers.Responsibility}](#${Headers.Responsibility.toLowerCase()})\n`; - markdown += `6. [${Headers.UCA}](#${Headers.UCA.toLowerCase()})\n`; - markdown += `7. [${Headers.ControllerConstraint}](#${Headers.ControllerConstraint.toLowerCase().replace( + markdown += `6. [${Headers.ControlStructure}](#${Headers.ControlStructure.toLowerCase().replace(" ", "-")})\n`; + markdown += `7. [${Headers.Responsibility}](#${Headers.Responsibility.toLowerCase()})\n`; + markdown += `8. [${Headers.UCA}](#${Headers.UCA.toLowerCase()})\n`; + markdown += `9. [${Headers.ControllerConstraint}](#${Headers.ControllerConstraint.toLowerCase().replace( " ", "-" )})\n`; - markdown += `8. [${Headers.LossScenario}](#${Headers.LossScenario.toLowerCase().replace(" ", "-")})\n`; - markdown += `9. [${Headers.SafetyRequirement}](#${Headers.SafetyRequirement.toLowerCase().replace(" ", "-")})\n`; - markdown += `10. [${Headers.ControllerConstraint}](#${Headers.ControllerConstraint.toLowerCase().replace( + markdown += `10. [${Headers.LossScenario}](#${Headers.LossScenario.toLowerCase().replace(" ", "-")})\n`; + markdown += `11. [${Headers.SafetyRequirement}](#${Headers.SafetyRequirement.toLowerCase().replace(" ", "-")})\n`; + markdown += `12. [${Headers.ControllerConstraint}](#${Headers.ControllerConstraint.toLowerCase().replace( " ", "-" )})\n`; - markdown += `11. [${Headers.Summary}](#${Headers.Summary.toLowerCase().replace(" ", "-").replace(" ", "-")})\n`; + markdown += `13. [${Headers.Summary}](#${Headers.Summary.toLowerCase().replace(" ", "-").replace(" ", "-")})\n`; return markdown; } diff --git a/extension/src/report/utils.ts b/extension/src/report/utils.ts index 0747c6a..74405e4 100644 --- a/extension/src/report/utils.ts +++ b/extension/src/report/utils.ts @@ -19,6 +19,8 @@ * The Headers for the STPA result file. */ export class Headers { + static Goal = "Goal"; + static Assumption = "Assumptions"; static Loss = "Losses"; static Hazard = "Hazards"; static SystemLevelConstraint = "System-level Constraints"; @@ -58,6 +60,8 @@ export const SIZE_MULTIPLIER = 0.85; export class StpaResult { title: string; + goals: StpaComponent[] = []; + assumptions: StpaComponent[] = []; losses: StpaComponent[] = []; hazards: StpaComponent[] = []; systemLevelConstraints: StpaComponent[] = [];