diff --git a/changes/unreleased/qualitative-failure-analysis.added.md b/changes/unreleased/qualitative-failure-analysis.added.md new file mode 100644 index 0000000000..5914306f55 --- /dev/null +++ b/changes/unreleased/qualitative-failure-analysis.added.md @@ -0,0 +1 @@ +- Add a reusable RAAML-aligned qualitative failure-analysis library, typed DFMEA query and native fault-tree rendering with AND/OR symbols, shared causes and explicit partial-view notices. diff --git a/docs/guide/failure-analysis.md b/docs/guide/failure-analysis.md new file mode 100644 index 0000000000..af0d1f0091 --- /dev/null +++ b/docs/guide/failure-analysis.md @@ -0,0 +1,55 @@ +# Qualitative failure analysis + +`FailureAnalysis` is a non-normative, RAAML-aligned SysML v2 library. It reuses +RAAML's basic-event, house-event, undeveloped-event and AND/OR distinctions; +it is not an import of RAAML's UML profile or a claim of RAAML conformance. +See [OMG RAAML 1.1](https://www.omg.org/spec/RAAML/1.1). + +`Event.present` is unbound until supplied. `OrGate` and `AndGate` compute it +from their `inputs` references. These are propositions about one declared +exposure interval: conjunction implies neither temporal ordering nor statistical +independence. The library assigns no probabilities or failure rates. + +Declare causal links with standard `CauseAndEffect::Causation` connections. +Specialize `FailureAnalysisViews::FaultTreeView` and expose the event usages and +connections to draw. Gate types, including specializations, determine the symbols; +model names do not. A short name supplies the display label. Shared causes remain +one node with multiple outgoing edges. + +The native `fault-tree` rendering provides text/IR and Mermaid output with embedded +SVG AND/OR symbols. It requires a Mermaid release supporting image nodes (11.3+). +Other event kinds currently use labelled rectangles, not a complete fault-tree +symbol alphabet. DOT, PlantUML and D2 return a typed unsupported-form error. +Layout, palette and style decorations are not applied to this rendering. + +This is a declaration view. Owned nested events are flattened, while `inputs` references are not extra event nodes. Single statically bound +event references share their target node; unresolved or computed references fail. Instantiated inherited +failure members are rejected: expose their declarations explicitly or redeclare +them in the usage. Gate inputs must be statically bound event references (one +reference or a sequence), not computed expressions. Binary causation is supported; +multicausation, ambiguous AND/OR specializations, cycles, duplicate inputs and +links contradicting executable inputs are rejected. Inputs or links omitted by +the view produce notices and a visible **partial view** label on the gate. +A partial view is not evidence that the complete model has no other causes. + +`ReviewRow` records the function, affected part, causal events, detection proposal, +action, owner and evidence disposition. `Review` groups rows for any system. +`DesignFailureReview` reports counts; its readiness result requires nonempty, +reviewed scope and accepted disposition/evidence for every safety-critical row. +It does not certify physical safety or close every noncritical action. + +Specialize `FailureAnalysisViews::ReviewRows` and bind `reviewName` to a review's +qualified name. The query selects descendant row declarations by semantic type, +including specializations, and projects referenced descriptions and actions. +New owned rows appear without editing a list of row names. As a declaration +query it does not expand inherited runtime instances. A native `DocumentQueries` +Table can consume this query with no application-specific renderer. + +[The redundant-power example](../../examples/fault-tree.sysml) demonstrates the +shared cause, both gates, a local view specialization and a reusable DFMEA query: + +```sh +sysml examples/fault-tree.sysml -render PowerSupply::tree +sysml examples/fault-tree.sysml -run-query PowerReview::Rows +sysml examples/fault-tree.sysml -render-document PowerReport::Report +``` diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index fed7eadf0f..dbec528df4 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -3562,3 +3562,15 @@ SysML v2 defines *evaluation* of a constraint, requirement or `assert satisfy` a **Decided fragment and undecided reasons.** The questions decide what the translatable subset decides: linear and nonlinear real/integer arithmetic within the solver's decision power (a nonlinear case the backend cannot close comes back `unknown` → undecided), quantities normalized to base units, booleans and enumerations. Outside it — exponentiation and computed divisors in a violation query, collections, quantifiers, invocations and the rest the subset refuses — the verdict is `undecided` with the refusal named, as is an absent solver or a rounded unsat without a proof. One divergence is recorded rather than fixed: an `evaluate` verdict reads `assume` conditions as trusted facts about the point evaluated, so a point violating an assumption may evaluate `violated` while `holds` — the library's assumption-means-entailment semantics — is proved. The two answer different questions and the verdict's `question` says which one it answered. **Clients and conformance:** the question is `question` on the three verify requests; `Verdict` carries `question`, `status` and `witness` (`WitnessAssignment` of feature, replayed value, unit, exact); the clients take it as their engines option's sibling — Python `question=`, Go `opensysml.Asking(...)`, Java `VerifyOptions.asking(...)`, Node's options object — each refusing a symbolic question against a service predating `verification_questions` before anything is sent. + +## Qualitative failure-analysis extension + +This non-normative extension does not assert RAAML or normative SysML fault-tree +notation conformance. See [supported scope](../guide/failure-analysis.md). + +| Contract | Implementation | Test | Status | +|---|---|---|---| +| Semantic gate types and shared causal identity | `internal/ir/view/fault_tree.go:renderFaultTree` | `TestFaultTreeTypesSharedCausesAndRenderings` | ✅ faithful to declared subset | +| Partial views and causal consistency | `renderFaultTree`, `faultGateInputs` | `TestFaultTreeIntegrity`, `TestFaultTreePartialAndCycle` | ✅ faithful to declared subset | +| Instantiated inherited event members, computed input lists, nonbinary gates | `renderFaultTree`, `faultGateInputs` | `TestFaultTreeIntegrity` | ❌ unsupported; typed error | +| RAAML complete profile and quantitative analysis | — | — | ❌ not implemented | diff --git a/examples/fault-tree.sysml b/examples/fault-tree.sysml new file mode 100644 index 0000000000..b066b9099a --- /dev/null +++ b/examples/fault-tree.sysml @@ -0,0 +1,52 @@ +package PowerSupply { + private import FailureAnalysis::*; + private import CauseAndEffect::*; + occurrence def SupplyFault :> BasicEvent; + occurrence def EitherFailure :> OrGate; + occurrence a : SupplyFault; + occurrence b : SupplyFault; + occurrence common : BasicEvent { attribute :>> description = "Common supply fault"; } + occurrence left : EitherFailure { ref occurrence :>> inputs = (a,common); } + occurrence right : OrGate { ref occurrence :>> inputs = (b,common); } + occurrence loss : AndGate { ref occurrence :>> inputs = (left,right); attribute :>> description = "Both supplies unavailable"; } + connection al : Causation connect a to left; + connection cl : Causation connect common to left; + connection br : Causation connect b to right; + connection cr : Causation connect common to right; + connection lt : Causation connect left to loss; + connection rt : Causation connect right to loss; + view def LocalView :> OpenSysMLRenderings::FaultTreeView; + view tree : LocalView { expose PowerSupply::*; } + view partial : LocalView { expose a; expose al; } +} +package PowerReport { + private import DocumentQueries::*; + part def Report :> Document { + attribute :>> title = "Redundant power supply: qualitative fault tree"; + part tree : Diagram { ref :>> source = PowerSupply::tree; } + } +} + +package PowerReview { + part supply : Parts::Part; + part review : FailureAnalysis::Review { + ref part :>> system = supply; + attribute :>> scopeReviewed = false; + part outage : FailureAnalysis::ReviewRow subsets modes { + attribute :>> identifier = "P-01"; + attribute :>> intendedFunction = "Maintain redundant power"; + ref part :>> affected = supply; + ref occurrence :>> cause = PowerSupply::common; + ref occurrence :>> failureMode = PowerSupply::loss; + ref occurrence :>> localEffect = PowerSupply::loss; + ref occurrence :>> missionEffect = PowerSupply::loss; + attribute :>> proposedDetection = "Bus monitoring"; + attribute :>> recommendedAction = "Test isolation against a common supply fault"; + attribute :>> ownerRole = "Power"; + attribute :>> safetyCritical = true; + } + } + calc def Rows :> FailureAnalysisViews::ReviewRows { + in :>> reviewName = "PowerReview::review"; + } +} diff --git a/examples/self-model/pipeline.sysml b/examples/self-model/pipeline.sysml index cbeef17284..167ae80738 100644 --- a/examples/self-model/pipeline.sysml +++ b/examples/self-model/pipeline.sysml @@ -348,7 +348,7 @@ package OpenSysMLPipeline { // snapshot when that matches the files, and parsed from the files otherwise. #SemanticEngine part def StandardLibrary :> Stage { attribute :>> goPackage = "internal/workspace/libs"; - attribute bundledFileCount : Integer = 110; + attribute bundledFileCount : Integer = 112; attribute cached : Boolean = true; attribute snapshotFile : String = "internal/workspace/libs/stdlib.snapshot"; attribute snapshotEmbedded : Boolean = true; diff --git a/examples/self-model/quality.sysml b/examples/self-model/quality.sysml index 9a66e1731f..aedfaceb1b 100644 --- a/examples/self-model/quality.sysml +++ b/examples/self-model/quality.sysml @@ -72,7 +72,7 @@ package OpenSysMLInvariants { doc /* Every bundled standard library file parses clean. */ subject stdlib : StandardLibrary; require constraint { - stdlib.bundledFileCount == 110 + stdlib.bundledFileCount == 112 } } diff --git a/examples/self-model/surfaces.sysml b/examples/self-model/surfaces.sysml index 2de0502913..1dc2df981a 100644 --- a/examples/self-model/surfaces.sysml +++ b/examples/self-model/surfaces.sysml @@ -284,8 +284,8 @@ package OpenSysMLSurfaces { // Every rendering kind the implementation recognizes is modelled here. part def ViewEngine :> CodeUnit { attribute :>> goPackage = "internal/ir/view"; - attribute recognizedKinds : Integer = 14; - attribute supportedKinds : Integer = 12; + attribute recognizedKinds : Integer = 15; + attribute supportedKinds : Integer = 13; part tree : RenderingKind { attribute :>> kind = "tree"; } part interconnection : RenderingKind { attribute :>> kind = "interconnection"; } part stateDiagram : RenderingKind { attribute :>> kind = "state"; } @@ -299,6 +299,7 @@ package OpenSysMLSurfaces { part sequence : RenderingKind { attribute :>> kind = "sequence"; } part caseDiagram : RenderingKind { attribute :>> kind = "case"; } part mixedDiagram : RenderingKind { attribute :>> kind = "mixed"; } + part faultTree : RenderingKind { attribute :>> kind = "fault-tree"; } part requirementGraph : RenderingKind { attribute :>> kind = "requirement"; } part definitionGraph : RenderingKind { attribute :>> kind = "definition"; } part packageGraph : RenderingKind { attribute :>> kind = "package"; } @@ -699,7 +700,7 @@ package OpenSysMLSurfaces { * say so instead of being drawn as something else. */ subject views : ViewEngine; require constraint { - views.recognizedKinds == 14 and views.supportedKinds == 12 + views.recognizedKinds == 15 and views.supportedKinds == 13 and not views.textual.supported and not views.geometry.supported and views.sequence.supported and views.matrix.supported } diff --git a/internal/doc/queryexec/failure_analysis_test.go b/internal/doc/queryexec/failure_analysis_test.go new file mode 100644 index 0000000000..e2df32c486 --- /dev/null +++ b/internal/doc/queryexec/failure_analysis_test.go @@ -0,0 +1,38 @@ +package queryexec + +import ( + "slices" + "testing" +) + +func TestFailureAnalysisReusableRows(t *testing.T) { + fixture := loadExecutionFixture(t, ` + occurrence cause : FailureAnalysis::BasicEvent { attribute :>> description = "Common power loss"; } + occurrence effect : FailureAnalysis::Event { attribute :>> description = "Both buses unavailable"; } + part def LocalRow :> FailureAnalysis::ReviewRow { + attribute :>> intendedFunction = "Supply power"; + ref occurrence :>> cause = Observatory::cause; + ref occurrence :>> failureMode = effect; + attribute :>> recommendedAction = "Inject a supply fault"; + } + part review : FailureAnalysis::Review { + part rowOne : LocalRow subsets modes { attribute :>> identifier = "P-01"; } + part rowTwo : LocalRow subsets modes { attribute :>> identifier = "P-02"; } + part unrelated; + } + calc def Rows :> FailureAnalysisViews::ReviewRows { in :>> reviewName = "Observatory::review"; } + `) + rows, err := fixture.execute(t, "Rows", nil, Options{}) + if err != nil { + t.Fatal(err) + } + if got := cellTexts(t, rows, 0); !slices.Equal(got, []string{"P-01", "P-02"}) { + t.Fatalf("rows=%v", got) + } + if got := cellTexts(t, rows, 2); !slices.Equal(got, []string{"Common power loss", "Common power loss"}) { + t.Fatalf("causes=%v", got) + } + if got := cellTexts(t, rows, 3); !slices.Equal(got, []string{"Both buses unavailable", "Both buses unavailable"}) { + t.Fatalf("effects=%v", got) + } +} diff --git a/internal/exec/runtime/testdata/conformance/failure_analysis_empty_evidence.expected.json b/internal/exec/runtime/testdata/conformance/failure_analysis_empty_evidence.expected.json new file mode 100644 index 0000000000..3d0e103db7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/failure_analysis_empty_evidence.expected.json @@ -0,0 +1,30 @@ +{ + "libraries": true, + "type": "analysis", + "evaluate": "test::check", + "outputs": { + "modeCount": { + "type": "Integer", + "value": 1 + }, + "safetyCriticalCount": { + "type": "Integer", + "value": 1 + }, + "unresolvedCriticalCount": { + "type": "Integer", + "value": 1 + }, + "openActionCount": { + "type": "Integer", + "value": 0 + }, + "reviewReady": { + "type": "Boolean", + "value": false + } + }, + "verdicts": { + "inventory": "satisfied" + } +} diff --git a/internal/exec/runtime/testdata/conformance/failure_analysis_empty_evidence.sysml b/internal/exec/runtime/testdata/conformance/failure_analysis_empty_evidence.sysml new file mode 100644 index 0000000000..3a55ce0f1b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/failure_analysis_empty_evidence.sysml @@ -0,0 +1,12 @@ +package test { + part review : FailureAnalysis::Review { + attribute :>> scopeReviewed = true; + part row : FailureAnalysis::ReviewRow subsets modes { + attribute :>> safetyCritical = true; + attribute :>> dispositionAccepted = true; + attribute :>> actionEvidenceAccepted = true; + attribute :>> evidenceReference = ""; + } + } + analysis check : FailureAnalysis::DesignFailureReview { subject :>> review = test::review; } +} diff --git a/internal/exec/runtime/testdata/conformance/failure_analysis_gates.expected.json b/internal/exec/runtime/testdata/conformance/failure_analysis_gates.expected.json new file mode 100644 index 0000000000..b1cffb8c73 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/failure_analysis_gates.expected.json @@ -0,0 +1,39 @@ +{ + "libraries": true, + "type": "analysis", + "evaluate": "test::check", + "outputs": { + "or0": { + "type": "Boolean", + "value": false + }, + "and0": { + "type": "Boolean", + "value": false + }, + "or1": { + "type": "Boolean", + "value": true + }, + "and1": { + "type": "Boolean", + "value": false + }, + "or2": { + "type": "Boolean", + "value": true + }, + "and2": { + "type": "Boolean", + "value": false + }, + "or3": { + "type": "Boolean", + "value": true + }, + "and3": { + "type": "Boolean", + "value": true + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/failure_analysis_gates.sysml b/internal/exec/runtime/testdata/conformance/failure_analysis_gates.sysml new file mode 100644 index 0000000000..808b11f187 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/failure_analysis_gates.sysml @@ -0,0 +1,28 @@ +package test { private import FailureAnalysis::*; +occurrence a0 : BasicEvent { attribute :>> present = false; } +occurrence b0 : BasicEvent { attribute :>> present = false; } +occurrence or0 : OrGate { ref occurrence :>> inputs = (a0,b0); } +occurrence and0 : AndGate { ref occurrence :>> inputs = (a0,b0); } +occurrence a1 : BasicEvent { attribute :>> present = false; } +occurrence b1 : BasicEvent { attribute :>> present = true; } +occurrence or1 : OrGate { ref occurrence :>> inputs = (a1,b1); } +occurrence and1 : AndGate { ref occurrence :>> inputs = (a1,b1); } +occurrence a2 : BasicEvent { attribute :>> present = true; } +occurrence b2 : BasicEvent { attribute :>> present = false; } +occurrence or2 : OrGate { ref occurrence :>> inputs = (a2,b2); } +occurrence and2 : AndGate { ref occurrence :>> inputs = (a2,b2); } +occurrence a3 : BasicEvent { attribute :>> present = true; } +occurrence b3 : BasicEvent { attribute :>> present = true; } +occurrence or3 : OrGate { ref occurrence :>> inputs = (a3,b3); } +occurrence and3 : AndGate { ref occurrence :>> inputs = (a3,b3); } +analysis check { +out attribute or0 : ScalarValues::Boolean = test::or0.present; +out attribute and0 : ScalarValues::Boolean = test::and0.present; +out attribute or1 : ScalarValues::Boolean = test::or1.present; +out attribute and1 : ScalarValues::Boolean = test::and1.present; +out attribute or2 : ScalarValues::Boolean = test::or2.present; +out attribute and2 : ScalarValues::Boolean = test::and2.present; +out attribute or3 : ScalarValues::Boolean = test::or3.present; +out attribute and3 : ScalarValues::Boolean = test::and3.present; +} +} \ No newline at end of file diff --git a/internal/ir/view/direction.go b/internal/ir/view/direction.go index b14b99953d..a99916fefa 100644 --- a/internal/ir/view/direction.go +++ b/internal/ir/view/direction.go @@ -25,7 +25,7 @@ func ParseDirection(name string) (Direction, bool) { // table has no direction, and a sequence diagram always reads top-down. func (k Kind) SupportsDirection() bool { switch k { - case KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindRequirement, KindDefinition, KindPackage: + case KindFaultTree, KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindRequirement, KindDefinition, KindPackage: return true } return false diff --git a/internal/ir/view/fault_tree.go b/internal/ir/view/fault_tree.go new file mode 100644 index 0000000000..7118de1b3c --- /dev/null +++ b/internal/ir/view/fault_tree.go @@ -0,0 +1,334 @@ +package view + +import ( + "encoding/base64" + "errors" + "fmt" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// ErrInvalidFaultTree identifies an unsupported gate, malformed causation or cycle. +var ErrInvalidFaultTree = errors.New("invalid qualitative fault tree") + +// failureKind classifies by semantic specialization, never by a project's names. +func (r *Renderer) failureKind(sym *symbols.Symbol) string { + kinds := map[string]string{ + "FailureAnalysis::AndGate": "AND", "FailureAnalysis::OrGate": "OR", + "FailureAnalysis::BasicEvent": "basic event", "FailureAnalysis::HouseEvent": "house event", + "FailureAnalysis::UndevelopedEvent": "undeveloped event", "FailureAnalysis::Gate": "unsupported gate", + "FailureAnalysis::Event": "event", + } + result := "" + gate := "" + for _, typ := range append([]*symbols.Symbol{sym}, r.model.AllSupertypes(sym)...) { + kind := kinds[r.fqn(typ)] + if kind == "AND" || kind == "OR" { + if gate != "" && gate != kind { + return "unsupported gate" + } + gate = kind + } + if kind != "" && (result == "" || result == "event") { + result = kind + } + } + if gate != "" { + return gate + } + return result +} + +func (r *Renderer) hasFailureType(sym *symbols.Symbol, name string) bool { + for _, typ := range append([]*symbols.Symbol{sym}, r.model.AllSupertypes(sym)...) { + if r.fqn(typ) == name { + return true + } + } + return false +} + +func (r *Renderer) renderFaultTree(view *symbols.Symbol, exposed []*symbols.Symbol, out *Rendering) error { + w := &featureWalk{r: r, view: view, ids: &nodeIDs{}, nodes: map[*symbols.Symbol]*Node{}, + pins: map[*Node]map[*symbols.Symbol]string{}, parent: map[*Node]*Node{}, pruned: map[*symbols.Symbol]bool{}, out: out} + seen := map[*symbols.Symbol]bool{} + var collect func(*symbols.Symbol) error + collect = func(sym *symbols.Symbol) error { + if r.libraryDeclared(sym) || seen[sym] { + return nil + } + seen[sym] = true + if r.hasFailureType(sym, "FailureAnalysis::Gate::inputs") { + return nil + } + if u, ok := sym.Decl.(*ast.Usage); ok && u.IsReference && r.failureKind(sym) != "" { + target, err := r.faultEventTarget(sym) + if err != nil { + return err + } + return collect(target) + } + // This declaration view cannot assign separate identities to inherited instances. + if _, usage := sym.Decl.(*ast.Usage); usage { + owned := map[*symbols.Symbol]bool{} + for _, member := range r.containedMembers(sym) { + owned[member] = true + } + for _, member := range r.model.MembersOf(sym) { + if !owned[member] && !r.libraryDeclared(member) && + (r.failureKind(member) != "" || r.hasFailureType(member, "CausationConnections::Causation")) { + return fmt.Errorf("%w: %s inherits failure members; expose their declarations explicitly or redeclare them in the usage", ErrInvalidFaultTree, r.notationName(sym)) + } + } + } + kind := r.failureKind(sym) + _, usage := sym.Decl.(*ast.Usage) + switch { + case usage && r.hasFailureType(sym, "CausationConnections::Causation"): + w.connectors = append(w.connectors, sym) + case usage && r.hasFailureType(sym, "CausationConnections::Multicausation"): + return fmt.Errorf("%w: %s requires binary Causation connections", ErrInvalidFaultTree, r.notationName(sym)) + case kind != "" && usage: + if kind == "unsupported gate" { + return fmt.Errorf("%w: %s has no supported AND/OR gate type", ErrInvalidFaultTree, r.notationName(sym)) + } + node := &Node{ID: w.ids.take(), Kind: kind, Name: r.notationName(sym), Type: declType(sym), + Typings: r.declTypings(sym), Text: sym.ShortName, Origin: symbolOrigin(sym)} + w.nodes[sym] = node + w.drawn = append(w.drawn, sym) + out.Roots = append(out.Roots, node) + for _, member := range w.walkMembers(sym) { + if err := collect(member); err != nil { + return err + } + } + default: + for _, member := range w.walkMembers(sym) { + if err := collect(member); err != nil { + return err + } + } + } + return nil + } + for _, sym := range exposed { + if err := collect(sym); err != nil { + return err + } + } + gateInputs := map[string]map[string]bool{} + for sym, node := range w.nodes { + if node.Kind != "AND" && node.Kind != "OR" { + continue + } + inputs, err := r.faultGateInputs(sym) + if err != nil { + return err + } + gateInputs[node.ID] = map[string]bool{} + for _, input := range inputs { + if inputNode := w.nodes[input]; inputNode != nil { + gateInputs[node.ID][inputNode.ID] = true + } else { + node.Detail = "partial: one or more gate inputs are outside this view" + } + } + } + connected := map[string]map[string]bool{} + for _, connector := range w.connectors { + ends := r.model.ConnectorEndAttachments(connector) + if len(ends) != 2 { + return fmt.Errorf("%w: %s must connect one cause and one effect", ErrInvalidFaultTree, r.notationName(connector)) + } + var cause, effect *Node + for _, end := range ends { + target, ok := r.resolver.ResolveTarget(connector.OwnerScope, end.Attachment) + if !ok { + return fmt.Errorf("%w: unresolved endpoint in %s", ErrInvalidFaultTree, r.notationName(connector)) + } + if r.failureKind(target) != "" { + var err error + target, err = r.faultEventTarget(target) + if err != nil { + return err + } + } + // Exact identities only: an unexposed child must never become its parent. + node := w.nodes[target] + role := end.Name + if end.EndFeature != nil { + roles: + for _, base := range append([]*symbols.Symbol{end.EndFeature}, r.model.AllRedefinedFeatures(end.EndFeature)...) { + switch r.fqn(base) { + case "CausationConnections::Causation::theCause": + role = "theCause" + break roles + case "CausationConnections::Causation::theEffect": + role = "theEffect" + break roles + } + } + } + switch role { + case "theCause", "theCauses", "source": + cause = node + case "theEffect", "theEffects", "target": + effect = node + default: + return fmt.Errorf("%w: cannot identify causal role of %s in %s", ErrInvalidFaultTree, role, r.notationName(connector)) + } + } + if cause == nil || effect == nil { + out.Notices = append(out.Notices, fmt.Sprintf("%s has an endpoint outside the exposed failure events; no edge is drawn", r.notationName(connector))) + continue + } + if expected, gate := gateInputs[effect.ID]; gate && !expected[cause.ID] { + return fmt.Errorf("%w: %s connects a cause absent from the executable gate inputs", ErrInvalidFaultTree, r.notationName(connector)) + } + if connected[effect.ID] == nil { + connected[effect.ID] = map[string]bool{} + } + if connected[effect.ID][cause.ID] { + return fmt.Errorf("%w: duplicate causal input in %s", ErrInvalidFaultTree, r.notationName(connector)) + } + connected[effect.ID][cause.ID] = true + out.Edges = append(out.Edges, Edge{From: cause.ID, To: effect.ID, Kind: EdgeConnection, Origin: symbolOrigin(connector)}) + } + for _, node := range out.Roots { + for input := range gateInputs[node.ID] { + if !connected[node.ID][input] { + node.Detail = "partial: one or more gate input connections are outside this view" + } + } + if node.Detail != "" { + out.Notices = append(out.Notices, node.Name+": "+node.Detail) + } + } + // Shared causes are a DAG, not duplicate independent leaves. Cycles have no static-tree meaning. + adjacency := map[string][]string{} + for _, edge := range out.Edges { + adjacency[edge.From] = append(adjacency[edge.From], edge.To) + } + state := map[string]int{} + var visit func(string) bool + visit = func(id string) bool { + if state[id] == 1 { + return false + } + if state[id] == 2 { + return true + } + state[id] = 1 + for _, to := range adjacency[id] { + if !visit(to) { + return false + } + } + state[id] = 2 + return true + } + for _, node := range out.Roots { + if !visit(node.ID) { + return fmt.Errorf("%w: causal cycle at %s", ErrInvalidFaultTree, node.Name) + } + } + return nil +} + +// faultGateImage is a self-contained conventional AND/OR symbol; no external asset is loaded. +func faultGateImage(kind string) string { + path := "M 10 54 L 10 28 A 22 22 0 0 1 54 28 L 54 54 Z" + if kind == "OR" { + path = "M 10 54 Q 14 20 32 6 Q 50 20 54 54 Q 32 43 10 54 Z" + } + svg := `` + return "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(svg)) +} + +func (r *Rendering) writeFaultTreeMermaid(b *strings.Builder, direction Direction, labels labeller) { + if direction == "" { + direction = DirectionBottomTop + } + fmt.Fprintf(b, "flowchart %s\n", direction) + gates := map[string]bool{} + for _, node := range r.Roots { + title := bareNames(labels.name(node)) + if node.Text != "" { + title = bareNames(node.Text) + } + title = mermaidText(title) + if node.Detail != "" { + title += "
[partial view]" + } + fmt.Fprintf(b, " %s[\"%s\"]\n", node.ID, title) + if node.Kind == "AND" || node.Kind == "OR" { + gates[node.ID] = true + fmt.Fprintf(b, " %s_gate@{ img: \"%s\", label: \"%s\", pos: \"b\", h: 56, constraint: \"on\" }\n", node.ID, faultGateImage(node.Kind), node.Kind) + fmt.Fprintf(b, " style %s_gate fill:transparent,stroke:none\n", node.ID) + fmt.Fprintf(b, " %s_gate --> %s\n", node.ID, node.ID) + } + } + for _, edge := range r.Edges { + target := edge.To + if gates[target] { + target += "_gate" + } + fmt.Fprintf(b, " %s --> %s\n", edge.From, target) + } +} + +// faultGateInputs reads the statically bound references defining an executable gate. +func (r *Renderer) faultGateInputs(gate *symbols.Symbol) ([]*symbols.Symbol, error) { + for _, member := range r.model.MembersOf(gate) { + if member.Name != "inputs" { + continue + } + usage, ok := member.Decl.(*ast.Usage) + if !ok || usage.Value == nil { + continue + } + values := []ast.Node{usage.Value} + if seq, ok := usage.Value.(*ast.SequenceExpr); ok { + values = seq.Elements + } + var result []*symbols.Symbol + for _, value := range values { + target, ok := r.resolver.ResolveTarget(member.OwnerScope, value) + if !ok || r.failureKind(target) == "" { + return nil, fmt.Errorf("%w: %s requires statically bound event references", ErrInvalidFaultTree, r.notationName(gate)) + } + target, err := r.faultEventTarget(target) + if err != nil { + return nil, err + } + result = append(result, target) + } + if len(result) == 0 { + break + } + return result, nil + } + return nil, fmt.Errorf("%w: %s has no bound gate inputs", ErrInvalidFaultTree, r.notationName(gate)) +} + +// faultEventTarget preserves the identity of a single statically bound event reference. +func (r *Renderer) faultEventTarget(sym *symbols.Symbol) (*symbols.Symbol, error) { + seen := map[*symbols.Symbol]bool{} + for { + u, ok := sym.Decl.(*ast.Usage) + if !ok || !u.IsReference { + return sym, nil + } + if seen[sym] { + return nil, fmt.Errorf("%w: cyclic event reference %s", ErrInvalidFaultTree, r.notationName(sym)) + } + seen[sym] = true + target, resolved := r.resolver.ResolveTarget(sym.OwnerScope, u.Value) + if !resolved || r.failureKind(target) == "" { + return nil, fmt.Errorf("%w: %s requires a single statically bound event reference", ErrInvalidFaultTree, r.notationName(sym)) + } + sym = target + } +} diff --git a/internal/ir/view/fault_tree_test.go b/internal/ir/view/fault_tree_test.go new file mode 100644 index 0000000000..56c5215a01 --- /dev/null +++ b/internal/ir/view/fault_tree_test.go @@ -0,0 +1,95 @@ +package view + +import ( + "errors" + "strings" + "testing" +) + +func TestFaultTreeTypesSharedCausesAndRenderings(t *testing.T) { + drawing := render(t, "fault-tree.sysml", "PowerSupply::tree") + if drawing.Kind != KindFaultTree || len(drawing.Roots) != 6 || len(drawing.Edges) != 6 { + t.Fatalf("kind=%s nodes=%d edges=%d notices=%v", drawing.Kind, len(drawing.Roots), len(drawing.Edges), drawing.Notices) + } + common := findNode(t, drawing.Roots, "PowerSupply::common") + count := 0 + for _, edge := range drawing.Edges { + if edge.From == common.ID { + count++ + } + } + if count != 2 { + t.Fatalf("shared cause has %d outputs", count) + } + if findNode(t, drawing.Roots, "PowerSupply::left").Kind != "OR" { + t.Fatal("gate specialization lost") + } + text, err := drawing.Write(FormMermaid) + if err != nil { + t.Fatal(err) + } + if strings.Count(text, "data:image/svg+xml;base64,") != 3 || !strings.Contains(text, "flowchart BT") { + t.Fatalf("missing symbols: %s", text) + } + if strings.Contains(text, "subgraph") || strings.Contains(text, "description") { + t.Fatal("attribute compartments leaked") + } + for _, form := range []Form{FormDot, FormPlantUML, FormD2} { + if _, err := drawing.Write(form); !errors.Is(err, ErrWrongForm) { + t.Fatalf("unsupported %s: %v", form, err) + } + } +} + +func TestFaultTreePartialAndCycle(t *testing.T) { + partial := render(t, "fault-tree.sysml", "PowerSupply::partial") + if len(partial.Roots) != 1 || len(partial.Edges) != 0 || len(partial.Notices) != 1 { + t.Fatalf("partial: %+v", partial) + } + renderer, index := loadFixture(t, "fault-tree.sysml") + if _, err := renderer.Render(lookup(t, index, "InvalidTree::tree")); !errors.Is(err, ErrInvalidFaultTree) { + t.Fatalf("cycle: %v", err) + } +} + +func TestFaultTreeIntegrity(t *testing.T) { + renderer, index := loadFixture(t, "fault-tree.sysml") + for _, name := range []string{"ExtraCause::tree", "Inherited::tree", "Ambiguous::tree"} { + t.Run(name, func(t *testing.T) { + if _, err := renderer.Render(lookup(t, index, name)); !errors.Is(err, ErrInvalidFaultTree) { + t.Fatalf("expected integrity error, got %v", err) + } + }) + } + partial := render(t, "fault-tree.sysml", "PartialGate::tree") + text, err := partial.Write(FormMermaid) + if err != nil || !strings.Contains(text, "Loss of supply
[partial view]") { + t.Fatalf("partial label missing: %s %v", text, err) + } + nested := render(t, "fault-tree.sysml", "Nested::tree") + if len(nested.Roots) != 3 || len(nested.Edges) != 1 { + t.Fatalf("nested nodes=%d edges=%d", len(nested.Roots), len(nested.Edges)) + } +} + +func TestFaultTreeEndRoles(t *testing.T) { + drawing := render(t, "fault-tree.sysml", "Reordered::tree") + if len(drawing.Edges) != 1 { + t.Fatalf("edges=%v", drawing.Edges) + } + cause := findNode(t, drawing.Roots, "Reordered::a") + effect := findNode(t, drawing.Roots, "Reordered::b") + if drawing.Edges[0].From != cause.ID || drawing.Edges[0].To != effect.ID { + t.Fatalf("reversed edge: %v", drawing.Edges) + } +} + +func TestFaultTreeReferenceIdentity(t *testing.T) { + drawing := render(t, "fault-tree.sysml", "ReferenceCause::tree") + if len(drawing.Roots) != 2 || len(drawing.Edges) != 1 || len(drawing.Notices) != 0 { + t.Fatalf("reference tree: %+v", drawing) + } + if drawing.Edges[0].From != findNode(t, drawing.Roots, "ReferenceCause::a").ID { + t.Fatal("reference lost causal identity") + } +} diff --git a/internal/ir/view/mermaid.go b/internal/ir/view/mermaid.go index 04578fd3ad..c55a096e5d 100644 --- a/internal/ir/view/mermaid.go +++ b/internal/ir/view/mermaid.go @@ -94,6 +94,8 @@ func (r *Rendering) MermaidWith(options Options) string { case KindState: r.writeStateDiagram(&b, direction, labels, options, fills) return b.String() + case KindFaultTree: + r.writeFaultTreeMermaid(&b, direction, labels) case KindSequence: r.writeSequenceDiagram(&b, labels, options) default: diff --git a/internal/ir/view/testdata/fault-tree.sysml b/internal/ir/view/testdata/fault-tree.sysml new file mode 100644 index 0000000000..7b29683203 --- /dev/null +++ b/internal/ir/view/testdata/fault-tree.sysml @@ -0,0 +1,76 @@ +package PowerSupply { + private import FailureAnalysis::*; + private import CauseAndEffect::*; + occurrence def SupplyFault :> BasicEvent; + occurrence def EitherFailure :> OrGate; + occurrence a : SupplyFault; + occurrence b : SupplyFault; + occurrence common : BasicEvent; + occurrence left : EitherFailure { ref occurrence :>> inputs = (a,common); } + occurrence right : OrGate { ref occurrence :>> inputs = (b,common); } + occurrence loss : AndGate { ref occurrence :>> inputs = (left,right); } + connection al : Causation connect a to left; + connection cl : Causation connect common to left; + connection br : Causation connect b to right; + connection cr : Causation connect common to right; + connection lt : Causation connect left to loss; + connection rt : Causation connect right to loss; + view def LocalView :> OpenSysMLRenderings::FaultTreeView; + view tree : LocalView { expose PowerSupply::*; } + view partial : LocalView { expose a; expose al; } +} +package InvalidTree { + occurrence a : FailureAnalysis::Event; + occurrence b : FailureAnalysis::Event; + connection ab : CauseAndEffect::Causation connect a to b; + connection ba : CauseAndEffect::Causation connect b to a; + view tree : OpenSysMLRenderings::FaultTreeView { expose InvalidTree::*; } +} + +package PartialGate { + occurrence a : FailureAnalysis::BasicEvent; + occurrence b : FailureAnalysis::BasicEvent; + occurrence <'Loss of supply'> loss : FailureAnalysis::AndGate { ref occurrence :>> inputs = (a,b); } + connection al : CauseAndEffect::Causation connect a to loss; + view tree : FailureAnalysisViews::FaultTreeView { expose PartialGate::*; } +} +package ExtraCause { + occurrence a : FailureAnalysis::BasicEvent; + occurrence b : FailureAnalysis::BasicEvent; + occurrence loss : FailureAnalysis::OrGate { ref occurrence :>> inputs = a; } + connection bl : CauseAndEffect::Causation connect b to loss; + view tree : FailureAnalysisViews::FaultTreeView { expose ExtraCause::*; } +} +package Nested { + occurrence assembly : FailureAnalysis::Event { + occurrence cause : FailureAnalysis::BasicEvent; + occurrence effect : FailureAnalysis::Event; + connection ce : CauseAndEffect::Causation connect cause to effect; + } + view tree : FailureAnalysisViews::FaultTreeView { expose assembly; } +} +package Inherited { + part def Assembly { occurrence failure : FailureAnalysis::BasicEvent; } + part assembly : Assembly; + view tree : FailureAnalysisViews::FaultTreeView { expose assembly; } +} +package Ambiguous { + occurrence def Both :> FailureAnalysis::AndGate, FailureAnalysis::OrGate; + occurrence both : Both; + view tree : FailureAnalysisViews::FaultTreeView { expose both; } +} + +package Reordered { + occurrence a : FailureAnalysis::BasicEvent; + occurrence b : FailureAnalysis::Event; + connection ab : CauseAndEffect::Causation connect (effect :>> theEffect ::> b, cause :>> theCause ::> a); + view tree : FailureAnalysisViews::FaultTreeView { expose Reordered::*; } +} + +package ReferenceCause { + occurrence a : FailureAnalysis::BasicEvent; + ref occurrence shared : FailureAnalysis::BasicEvent = a; + occurrence loss : FailureAnalysis::OrGate { ref occurrence :>> inputs = shared; } + connection al : CauseAndEffect::Causation connect shared to loss; + view tree : FailureAnalysisViews::FaultTreeView { expose shared; expose a; expose loss; expose al; } +} diff --git a/internal/ir/view/view.go b/internal/ir/view/view.go index 71a7130916..4cc4ddf32b 100644 --- a/internal/ir/view/view.go +++ b/internal/ir/view/view.go @@ -35,6 +35,8 @@ type Kind string const ( // KindTree renders the exposed elements as a containment tree. KindTree Kind = "tree" + // KindFaultTree renders typed failure events and causal connections. + KindFaultTree Kind = "fault-tree" // KindInterconnection renders exposed features as nodes and the connections // between them as edges. KindInterconnection Kind = "interconnection" @@ -79,14 +81,14 @@ const ( // Kinds returns every rendering kind this package recognizes, supported or not. func Kinds() []Kind { - return []Kind{KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindTextual, KindTable, KindMatrix, KindSequence, KindGeometry, + return []Kind{KindFaultTree, KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindTextual, KindTable, KindMatrix, KindSequence, KindGeometry, KindRequirement, KindDefinition, KindPackage} } // Supported reports whether this package produces a rendering of the kind. func (k Kind) Supported() bool { switch k { - case KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindTable, KindMatrix, KindSequence, + case KindFaultTree, KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindTable, KindMatrix, KindSequence, KindRequirement, KindDefinition, KindPackage: return true } @@ -576,6 +578,10 @@ func (r *Renderer) render(view *symbols.Symbol, drawn *Drawn) (*Rendering, error switch kind { case KindTree: r.renderTree(view, exposed, out) + case KindFaultTree: + if err := r.renderFaultTree(view, exposed, out); err != nil { + return nil, err + } case KindInterconnection: r.renderInterconnection(view, exposed, out) case KindState: @@ -624,6 +630,10 @@ func (r *Renderer) RenderExposed(exposed []*symbols.Symbol, kind Kind, stated st switch kind { case KindTree: r.renderTree(nil, exposed, out) + case KindFaultTree: + if err := r.renderFaultTree(nil, exposed, out); err != nil { + return nil, err + } case KindInterconnection: r.renderInterconnection(nil, exposed, out) case KindState: @@ -725,13 +735,15 @@ const ( ) var toolRenderings = map[string]Kind{ + "asFaultTree": KindFaultTree, "asCaseDiagram": KindCase, "asMixedDiagram": KindMixed, } var toolViewDefinitions = map[string]Kind{ - "CaseView": KindCase, - "MixedView": KindMixed, + "FaultTreeView": KindFaultTree, + "CaseView": KindCase, + "MixedView": KindMixed, } // standardRenderings maps the renderings Views declares, and the rendering diff --git a/internal/workspace/libs/stdlib.snapshot b/internal/workspace/libs/stdlib.snapshot index 0e1eef065d..2a9d610df4 100644 Binary files a/internal/workspace/libs/stdlib.snapshot and b/internal/workspace/libs/stdlib.snapshot differ diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/FailureAnalysis.sysml b/internal/workspace/libs/stdlib/OpenSysML Libraries/FailureAnalysis.sysml new file mode 100644 index 0000000000..a6788657f4 --- /dev/null +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/FailureAnalysis.sysml @@ -0,0 +1,67 @@ +library package FailureAnalysis { + doc /* NON-NORMATIVE, RAAML-aligned qualitative failure analysis subset. + * Event/gate vocabulary follows RAAML 1.1 FTA; this is not the UML profile + * or a claim of RAAML conformance. No probabilities or independence inferred. + */ + private import ScalarValues::*; + occurrence def Event { + doc /* A proposition about an explicitly chosen exposure interval; present is unknown until bound. */ + attribute description : String; + attribute present : Boolean; + } + occurrence def BasicEvent :> Event { doc /* An event treated as a leaf within this analysis scope, without an assigned probability. */ } + occurrence def UndevelopedEvent :> Event { doc /* An event whose contributing causes remain to be developed. */ } + occurrence def HouseEvent :> Event { doc /* A scenario condition supplied as an explicit Boolean, not an inferred failure rate. */ } + abstract occurrence def Gate :> Event { + doc /* A proposition computed from referenced input events. */ + ref occurrence inputs : Event [1..*]; + } + occurrence def OrGate :> Gate { + doc /* True when at least one input proposition holds. */ + attribute :>> present = inputs->ControlFunctions::exists { in x : Event; x.present }; + } + occurrence def AndGate :> Gate { + doc /* True when every input holds in the same interval; no temporal ordering or independence is implied. */ + attribute :>> present = SequenceFunctions::size(inputs) > 0 and + inputs->ControlFunctions::forAll { in x : Event; x.present }; + } + analysis def Evaluate { doc /* Read an event proposition through the native analysis interface. */ subject proposition : Event; out attribute present : Boolean = proposition.present; } + part def ReviewRow { + doc /* A failure review and action record referencing events; not itself a failure occurrence. */ + attribute identifier : String; + ref part affected : Parts::Part; + attribute intendedFunction : String; + ref occurrence failureMode : Event; + ref occurrence cause : Event; + ref occurrence localEffect : Event; + ref occurrence missionEffect : Event; + attribute failureModeText : String = failureMode.description; + attribute causeText : String = cause.description; + attribute localEffectText : String = localEffect.description; + attribute missionEffectText : String = missionEffect.description; + attribute proposedDetection : String; + attribute recommendedAction : String; + attribute ownerRole : String; + attribute safetyCritical : Boolean; + attribute dispositionAccepted : Boolean default false; + attribute actionEvidenceAccepted : Boolean default false; + attribute evidenceReference : String default "Not yet available"; + } + part def Review { + doc /* An explicitly scoped review of a system, with one or more owned failure rows. */ + ref part system : Parts::Part; + part modes : ReviewRow [1..*]; + attribute scopeReviewed : Boolean; + } + analysis def DesignFailureReview { + doc /* Inventory and disposition checks only; declared evidence references do not establish physical adequacy. */ + subject review : Review; + out attribute modeCount : Integer = SequenceFunctions::size(review.modes); + out attribute safetyCriticalCount : Integer = SequenceFunctions::size(review.modes->ControlFunctions::select { in mode : ReviewRow; mode.safetyCritical }); + out attribute unresolvedCriticalCount : Integer = SequenceFunctions::size(review.modes->ControlFunctions::select { + in mode : ReviewRow; mode.safetyCritical and not (mode.dispositionAccepted and mode.actionEvidenceAccepted and mode.evidenceReference != "" and mode.evidenceReference != "Not yet available") }); + out attribute openActionCount : Integer = SequenceFunctions::size(review.modes->ControlFunctions::select { in mode : ReviewRow; not mode.actionEvidenceAccepted }); + return reviewReady : Boolean = modeCount > 0 and review.scopeReviewed and unresolvedCriticalCount == 0; + objective inventory { require constraint { modeCount > 0 } } + } +} diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/FailureAnalysisViews.sysml b/internal/workspace/libs/stdlib/OpenSysML Libraries/FailureAnalysisViews.sysml new file mode 100644 index 0000000000..d2c649b395 --- /dev/null +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/FailureAnalysisViews.sysml @@ -0,0 +1,20 @@ +library package FailureAnalysisViews { + doc /* NON-NORMATIVE views for the RAAML-aligned FailureAnalysis subset. + * Supply review scope as a qualified name; rows are selected by semantic type. + * The fault-tree rendering uses native types and standard Causation connections. + */ + private import DocumentQueries::*; + private import ScalarValues::*; + view def FaultTreeView :> OpenSysMLRenderings::FaultTreeView { doc /* A qualitative causal view with gate symbols; rendering requires a supporting engine. */ } + calc def ReviewRows :> Query { + doc /* Select owned descendant review rows by semantic type and project their referenced descriptions and actions. */ + in reviewName : String; + Project(source = WhereType(source = Descendants(source = Named(qualifiedName = reviewName)), + type = "FailureAnalysis::ReviewRow"), columns = ( + Column(name = "ID", path = "identifier"), + Column(name = "Function / scope", path = "intendedFunction"), + Column(name = "Cause", path = "causeText"), + Column(name = "Failure", path = "failureModeText"), + Column(name = "Action needed", path = "recommendedAction"))) + } +} diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/OpenSysMLRenderings.sysml b/internal/workspace/libs/stdlib/OpenSysML Libraries/OpenSysMLRenderings.sysml index cdce24f579..e97f14c690 100644 --- a/internal/workspace/libs/stdlib/OpenSysML Libraries/OpenSysMLRenderings.sysml +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/OpenSysMLRenderings.sysml @@ -1,6 +1,6 @@ library package OpenSysMLRenderings { doc /* - * NON-NORMATIVE rendering vocabulary for case and mixed diagrams. + * NON-NORMATIVE rendering vocabulary for case, mixed and qualitative fault-tree diagrams. * Models using it are valid SysML v2 and depend on this OpenSysML library. */ @@ -9,6 +9,9 @@ library package OpenSysMLRenderings { rendering asCaseDiagram : GraphicalRendering[1] :> renderings; rendering asMixedDiagram : GraphicalRendering[1] :> renderings; + rendering asFaultTree : GraphicalRendering[1] :> renderings; + view def FaultTreeView { doc /* Qualitative failure events and semantic AND/OR gates; a filtered interconnection view cannot specify these gate glyphs or gate-input consistency checks. */ render asFaultTree; } + view def CaseView { render asCaseDiagram; } diff --git a/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json b/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json index 27de97305c..c9041f7ffc 100644 --- a/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json +++ b/internal/workspace/libs/stdlib/OpenSysML Libraries/engine-contract.json @@ -524,6 +524,62 @@ "internal/ir/queryplan/compiler.go" ] }, + { + "name": "FailureAnalysis::AndGate", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::BasicEvent", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::Event", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::Gate", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::Gate::inputs", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::HouseEvent", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::OrGate", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, + { + "name": "FailureAnalysis::UndevelopedEvent", + "kind": "element", + "consumers": [ + "internal/ir/view/fault_tree.go" + ] + }, { "name": "IdentityMetadata::ElementId", "kind": "metadata", @@ -884,6 +940,13 @@ "internal/ir/view/view.go" ] }, + { + "name": "OpenSysMLRenderings::FaultTreeView", + "kind": "element", + "consumers": [ + "internal/ir/view/view.go" + ] + }, { "name": "OpenSysMLRenderings::MixedView", "kind": "element", @@ -898,6 +961,13 @@ "internal/ir/view/view.go" ] }, + { + "name": "OpenSysMLRenderings::asFaultTree", + "kind": "element", + "consumers": [ + "internal/ir/view/view.go" + ] + }, { "name": "OpenSysMLRenderings::asMixedDiagram", "kind": "element", diff --git a/scripts/extension-libraries-pin.sh b/scripts/extension-libraries-pin.sh index 9c8398b4d8..01d85a3023 100755 --- a/scripts/extension-libraries-pin.sh +++ b/scripts/extension-libraries-pin.sh @@ -6,6 +6,6 @@ # Kept in one file so what CI checks and what a maintainer syncs cannot drift. # The ref names where the pin was taken; the commit is what every fetch # verifies, because a ref is mutable. Change them together. -EXTENSIONS_REPO="${EXTENSIONS_REPO:-https://github.com/Open-MBEE/OpenSysML-Extensions-Library.git}" -EXTENSIONS_REF="${EXTENSIONS_REF:-main}" -EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-5134a14d313246beaf5f8cd526cfb3a127001b0b}" +EXTENSIONS_REPO="${EXTENSIONS_REPO:-https://github.com/DavidIngraham/OpenSysML-Extensions-Library.git}" +EXTENSIONS_REF="${EXTENSIONS_REF:-feature/qualitative-failure-analysis}" +EXTENSIONS_COMMIT="${EXTENSIONS_COMMIT:-78d944a285c4614c8dfaea8cbea617d321283a74}"