diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_failure.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_failure.go new file mode 100644 index 00000000..1ecc5b0e --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_failure.go @@ -0,0 +1,29 @@ +package handlers + +import ( + "net/http" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/gin-gonic/gin" +) + +// ListFailureKnowledge handles GET /failure-knowledge. +// Read-only unified failure-knowledge ontology (Component → FailureMode → +// Mechanism → Effect → Hazard → Harm → Control) curated from commercially-usable +// open sources (currently NASA NTRS, public domain). Optional ?domain= filter. +// This is the shared corpus that seeds the FMEA library and the CE hazard side. +func (h *IACEHandler) ListFailureKnowledge(c *gin.Context) { + var items []iace.FailureKnowledge + if d := c.Query("domain"); d != "" { + items = iace.FailureKnowledgeByDomain(d) + } else { + items = iace.AllFailureKnowledge() + } + if items == nil { + items = []iace.FailureKnowledge{} + } + c.JSON(http.StatusOK, gin.H{ + "failure_knowledge": items, + "total": len(items), + }) +} diff --git a/ai-compliance-sdk/internal/app/routes_iace.go b/ai-compliance-sdk/internal/app/routes_iace.go index 8e0f0001..f7bb02f2 100644 --- a/ai-compliance-sdk/internal/app/routes_iace.go +++ b/ai-compliance-sdk/internal/app/routes_iace.go @@ -31,6 +31,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.GET("/component-library", h.ListComponentLibrary) iaceRoutes.GET("/energy-sources", h.ListEnergySources) iaceRoutes.GET("/minimum-distances", h.ListMinimumDistances) + iaceRoutes.GET("/failure-knowledge", h.ListFailureKnowledge) iaceRoutes.GET("/tags", h.ListTags) iaceRoutes.GET("/hazard-patterns", h.ListHazardPatterns) diff --git a/ai-compliance-sdk/internal/iace/failure_knowledge.go b/ai-compliance-sdk/internal/iace/failure_knowledge.go new file mode 100644 index 00000000..622d8fbc --- /dev/null +++ b/ai-compliance-sdk/internal/iace/failure_knowledge.go @@ -0,0 +1,128 @@ +package iace + +import "strings" + +// FailureKnowledge is one curated, source-attributed failure record in the +// UNIFIED ontology shared by the FMEA library and the CE hazard side: +// +// Component → FailureMode → (Mechanism) → Effect → Hazard → Harm → Control +// +// Every source (NASA, OSHA, CPSC, MAUDE, NTSB …) maps into THIS one schema with +// a licence tag. The licence allowlist (FailureKnowledgeLicenseAllowed) rejects +// non-commercial / copyrighted / proprietary sources up front — the same +// discipline that kept FMD-91/NPRD-91 (© IITRI) and DIN/ISO out. +type FailureKnowledge struct { + ID string `json:"id"` // FK-NASA-0001 + Component string `json:"component"` // canonical → component library + FailureMode string `json:"failure_mode"` // canonical → mode taxonomy + Mechanism string `json:"mechanism"` // cause: fatigue, contamination, wear… + Effect string `json:"effect"` // system-level effect + Hazard string `json:"hazard,omitempty"` // EN ISO 12100 category; "" = pure reliability + Harm string `json:"harm,omitempty"` + Control string `json:"control"` // recommended action / mitigation + Domain string `json:"domain"` // space/aviation/medical/consumer/industrial/general + Source string `json:"source"` + License string `json:"license"` + Attribution string `json:"attribution"` + URL string `json:"url,omitempty"` +} + +// FailureKnowledgeLicenseAllowed reports whether a licence string is a +// commercially-usable open licence. Allowlist by keyword; anything signalling +// "all rights reserved", non-commercial, or a known copyrighted/proprietary +// source is rejected. +func FailureKnowledgeLicenseAllowed(license string) bool { + l := strings.ToLower(license) + // Hard rejects first (a copyrighted source may also say "public"…). + for _, bad := range []string{ + "all rights reserved", "non-commercial", "noncommercial", "cc by-nc", "cc-by-nc", + "-nd", "no derivatives", "proprietary", "iitri", "quanterion", "oreda", + "din ", "beuth", "iso ", "iec ", "aiag", "vda", "sae j", "dguv", + } { + if strings.Contains(l, bad) { + return false + } + } + for _, ok := range []string{ + "public domain", "public_use_permitted", "gov_public", "cc0", + "cc by 4", "cc-by 4", "cc by-sa", "cc-by-sa", + "open government licence", "ogl", "mit", "apache", "bsd", + "reproduction authorised", "reproduction authorized", + } { + if strings.Contains(l, ok) { + return true + } + } + return false +} + +// GetNASAFailureKnowledge returns the curated NASA lessons-learned starter set +// (US-Gov public domain, NTRS). Each entry is anchored on a real NTRS document; +// fields the source did not state are left empty rather than guessed. +func GetNASAFailureKnowledge() []FailureKnowledge { + const lic = "Public Domain (NASA NTRS, GOV_PUBLIC_USE_PERMITTED)" + ntrs := func(id string) string { + return "https://ntrs.nasa.gov/api/citations/" + id + "/downloads/" + id + ".pdf" + } + return []FailureKnowledge{ + { + ID: "FK-NASA-0001", Component: "flow_control_valve_poppet", FailureMode: "fracture", + Mechanism: "high-cycle fatigue / contamination", Effect: "sluggish valve response, loss of flow control", + Control: "contamination control + design margin against resonant fatigue", Domain: "propulsion", + Source: "NASA NTRS 20110013003", License: lic, + Attribution: "NASA, Lessons Learned from the SSME Hydrogen Flow Control Valve Poppet Breakage (public domain)", + URL: ntrs("20110013003"), + }, + { + ID: "FK-NASA-0002", Component: "control_moment_gyroscope", FailureMode: "mechanical_failure", + Mechanism: "in-service degradation (under investigation)", Effect: "loss of attitude-control actuator", + Control: "redundancy + condition monitoring + return-for-failure-analysis", Domain: "spacecraft", + Source: "NASA NTRS 20100021932", License: lic, + Attribution: "NASA, Space Station Control Moment Gyroscope Lessons Learned (public domain)", + URL: ntrs("20100021932"), + }, + { + ID: "FK-NASA-0003", Component: "composite_structure", FailureMode: "fracture", + Mechanism: "accumulation/propagation of damage", Effect: "load-carrying capability below required → structural failure", + Hazard: "mechanical_hazard", Harm: "structural collapse", Control: "damage-tolerance design + inspection", + Domain: "structures", Source: "NASA NTRS 20080015747", License: lic, + Attribution: "NASA, Lessons Learned from Recent Failure and Incident Investigations (public domain)", + URL: ntrs("20080015747"), + }, + { + ID: "FK-NASA-0004", Component: "pressurized_garment", FailureMode: "ignition", + Mechanism: "flash fire during functional test", Effect: "unit destroyed", + Hazard: "fire_explosion", Harm: "fire", Control: "oxygen-fire control + material/ignition-source review", + Domain: "life_support", Source: "NASA NTRS 20230013281", License: lic, + Attribution: "NASA, Lessons Learned from the EMU Fire (public domain)", + URL: ntrs("20230013281"), + }, + { + ID: "FK-NASA-0005", Component: "fuel_cell", FailureMode: "loss_of_function", + Mechanism: "design conditions leading to component failure", Effect: "loss of electrical power generation", + Hazard: "electrical_hazard", Control: "design changes per identified failure mode", Domain: "power", + Source: "NASA NTRS 20090016297", License: lic, + Attribution: "NASA, Apollo CSM Power Generation System Design Considerations (public domain)", + URL: ntrs("20090016297"), + }, + } +} + +// AllFailureKnowledge aggregates every source's curated entries (currently NASA; +// OSHA/CPSC/MAUDE/NTSB will append here as they are added). +func AllFailureKnowledge() []FailureKnowledge { + var all []FailureKnowledge + all = append(all, GetNASAFailureKnowledge()...) + return all +} + +// FailureKnowledgeByDomain filters the corpus by domain (e.g. "industrial"). +func FailureKnowledgeByDomain(domain string) []FailureKnowledge { + var out []FailureKnowledge + for _, fk := range AllFailureKnowledge() { + if fk.Domain == domain { + out = append(out, fk) + } + } + return out +} diff --git a/ai-compliance-sdk/internal/iace/failure_knowledge_test.go b/ai-compliance-sdk/internal/iace/failure_knowledge_test.go new file mode 100644 index 00000000..58a46fef --- /dev/null +++ b/ai-compliance-sdk/internal/iace/failure_knowledge_test.go @@ -0,0 +1,82 @@ +package iace + +import "testing" + +func TestFailureKnowledgeLicenseAllowed(t *testing.T) { + accept := []string{ + "Public Domain (NASA NTRS, GOV_PUBLIC_USE_PERMITTED)", + "US Public Domain", + "CC BY 4.0", + "CC BY-SA 4.0", + "Open Government Licence v3.0", + "MIT", + } + for _, l := range accept { + if !FailureKnowledgeLicenseAllowed(l) { + t.Errorf("license should be ALLOWED: %q", l) + } + } + reject := []string{ + "© 1991, IIT Research Institute. All Rights Reserved.", // FMD-91/NPRD-91 + "CC BY-NC 4.0", + "proprietary (Quanterion)", + "DIN EN ISO 13849 table", + "AIAG-VDA handbook", + "OREDA member-only", + "CC BY-ND", + "", + } + for _, l := range reject { + if FailureKnowledgeLicenseAllowed(l) { + t.Errorf("license should be REJECTED: %q", l) + } + } +} + +func TestNASAFailureKnowledge_Integrity(t *testing.T) { + seen := map[string]bool{} + nasa := GetNASAFailureKnowledge() + if len(nasa) == 0 { + t.Fatal("NASA starter set is empty") + } + for _, fk := range nasa { + if seen[fk.ID] { + t.Errorf("duplicate FK id %q", fk.ID) + } + seen[fk.ID] = true + if fk.ID == "" || fk.Component == "" || fk.FailureMode == "" || fk.Effect == "" || + fk.Control == "" || fk.Source == "" || fk.License == "" || fk.Attribution == "" { + t.Errorf("%s: empty required field: %+v", fk.ID, fk) + } + if fk.URL == "" { + t.Errorf("%s: NASA entry missing source URL", fk.ID) + } + } +} + +// Governance invariant: EVERY curated entry must carry a commercially-usable +// licence — this is the gate that keeps copyrighted/proprietary data out. +func TestAllFailureKnowledge_LicensesAllowed(t *testing.T) { + for _, fk := range AllFailureKnowledge() { + if !FailureKnowledgeLicenseAllowed(fk.License) { + t.Errorf("%s carries a non-allowed licence %q", fk.ID, fk.License) + } + } +} + +func TestFailureKnowledgeByDomain(t *testing.T) { + all := AllFailureKnowledge() + if len(all) == 0 { + t.Fatal("no failure knowledge") + } + d := all[0].Domain + got := FailureKnowledgeByDomain(d) + if len(got) == 0 { + t.Errorf("expected entries for domain %q", d) + } + for _, fk := range got { + if fk.Domain != d { + t.Errorf("domain filter leaked %q into %q", fk.Domain, d) + } + } +}