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}"