feat(iace): add data-driven Architektur & Datenfluss explainer tab
Adds an auditor-facing view of the IACE engine: a clickable 10-stage pipeline flow (Grenzen-Formular → ParseNarrative → Pattern-Gates → Relevanz → Caps → Gefährdungen → Maßnahmen → Risiko → Normen → Matrix), plus live library counts, the data-source/license register (incl. the DIN/Beuth + DGUV exclusions), and the norm-matching logic that reconciles DIN/ISO/OSHA machine-type vocabulary via canonicalMachineType folding. Backend: BuildArchitecture() with LIVE counts so the diagram can never drift; GET /iace/architecture; collectAllNorms() extracted from SuggestNorms as the single source of truth for the norm-library count. Frontend: useArchitecture hook + page + new IACE nav tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
export interface ArchStage {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
summary: string
|
||||||
|
input: string
|
||||||
|
logic: string
|
||||||
|
data_source: string
|
||||||
|
example: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchLibrary {
|
||||||
|
name: string
|
||||||
|
count: number
|
||||||
|
source_file: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchDataSource {
|
||||||
|
name: string
|
||||||
|
license: string
|
||||||
|
usage: string
|
||||||
|
status: string // "verwendet" | "ausgeschlossen"
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RiskEvidence {
|
||||||
|
mode: string
|
||||||
|
label: string
|
||||||
|
stat: string
|
||||||
|
source: string
|
||||||
|
license: string
|
||||||
|
attribution: string
|
||||||
|
retrieved: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Architecture {
|
||||||
|
stages: ArchStage[]
|
||||||
|
libraries: ArchLibrary[]
|
||||||
|
data_sources: ArchDataSource[]
|
||||||
|
norm_matching: string[]
|
||||||
|
evidence: RiskEvidence[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Loads the data-driven IACE engine self-description (global, not per project). */
|
||||||
|
export function useArchitecture() {
|
||||||
|
const [data, setData] = useState<Architecture | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
async function load() {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/sdk/v1/iace/architecture')
|
||||||
|
const json = res.ok ? ((await res.json()) as Architecture) : null
|
||||||
|
if (!cancelled) setData(json)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load IACE architecture:', err)
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { data, loading }
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useArchitecture, type ArchStage } from './_hooks/useArchitecture'
|
||||||
|
|
||||||
|
export default function ArchitekturPage() {
|
||||||
|
const { data, loading } = useArchitecture()
|
||||||
|
const [open, setOpen] = useState<string | null>('grenzen')
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="text-sm text-gray-500 dark:text-gray-400 p-1">Lade Engine-Architektur…</div>
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <div className="text-sm text-red-600">Architektur konnte nicht geladen werden.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 max-w-[1100px]">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Architektur & Datenfluss</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-3xl mt-1">
|
||||||
|
Nachvollziehbar für Auditoren: <strong>woher jede Information stammt</strong> und{' '}
|
||||||
|
<strong>wie die Risikobeurteilung zustande kommt</strong> — jede Station, jedes Gate, jede
|
||||||
|
Bibliothek und Datenquelle, in Reihenfolge. Die Zahlen sind <strong>live</strong> aus der
|
||||||
|
laufenden Engine.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pipeline flow */}
|
||||||
|
<section className="space-y-2">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Deterministische Pipeline</h2>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{data.stages.map((s, i) => (
|
||||||
|
<StageRow
|
||||||
|
key={s.id}
|
||||||
|
stage={s}
|
||||||
|
last={i === data.stages.length - 1}
|
||||||
|
open={open === s.id}
|
||||||
|
onToggle={() => setOpen(open === s.id ? null : s.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Libraries */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Wissensbasen (Live-Bestand)</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
|
{data.libraries.map((l) => (
|
||||||
|
<div key={l.name} className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
|
||||||
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
|
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">{l.name}</span>
|
||||||
|
<span className="text-lg font-bold text-purple-600 tabular-nums">{l.count.toLocaleString('de-DE')}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{l.description}</p>
|
||||||
|
<code className="text-[10px] text-gray-400 mt-1 block">{l.source_file}</code>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Norm matching */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Normen-Matching (DIN / ISO / OSHA)</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{data.norm_matching.map((n, i) => (
|
||||||
|
<li key={i} className="flex gap-2 text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
<span className="text-purple-500 mt-0.5 shrink-0">▸</span>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: inlineCode(n) }} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Data sources & licenses */}
|
||||||
|
<section className="space-y-3">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Datenquellen & Lizenzen</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
|
||||||
|
<th className="py-1.5 pr-3">Quelle</th>
|
||||||
|
<th className="py-1.5 pr-3">Lizenz</th>
|
||||||
|
<th className="py-1.5 pr-3">Nutzung</th>
|
||||||
|
<th className="py-1.5">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.data_sources.map((d) => (
|
||||||
|
<tr key={d.name} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
|
||||||
|
<td className="py-1.5 pr-3 text-gray-700 dark:text-gray-300 font-medium">{d.name}</td>
|
||||||
|
<td className="py-1.5 pr-3 text-gray-500">{d.license}</td>
|
||||||
|
<td className="py-1.5 pr-3 text-gray-500">{d.usage}</td>
|
||||||
|
<td className="py-1.5">
|
||||||
|
<span
|
||||||
|
className={`inline-block rounded px-1.5 py-0.5 font-medium ${
|
||||||
|
d.status === 'verwendet'
|
||||||
|
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
|
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{d.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{data.evidence.length > 0 && (
|
||||||
|
<p className="text-[11px] text-gray-400">
|
||||||
|
Belegte Kontaktmodus-Quoten (ESAW):{' '}
|
||||||
|
{data.evidence.map((e) => `${e.label} ${e.stat}`).join(' · ')} — {data.evidence[0]?.attribution}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StageRow({
|
||||||
|
stage,
|
||||||
|
last,
|
||||||
|
open,
|
||||||
|
onToggle,
|
||||||
|
}: {
|
||||||
|
stage: ArchStage
|
||||||
|
last: boolean
|
||||||
|
open: boolean
|
||||||
|
onToggle: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={onToggle}
|
||||||
|
className={`w-full text-left rounded-lg border p-3 transition-colors ${
|
||||||
|
open
|
||||||
|
? 'border-purple-300 bg-purple-50/60 dark:border-purple-700 dark:bg-purple-900/20'
|
||||||
|
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-gray-800 dark:text-gray-200">{stage.title}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">{stage.summary}</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-gray-400 text-xs shrink-0">{open ? '▲' : '▼'}</span>
|
||||||
|
</div>
|
||||||
|
{open && (
|
||||||
|
<dl className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
||||||
|
<Field label="Input" value={stage.input} />
|
||||||
|
<Field label="Logik" value={stage.logic} />
|
||||||
|
<Field label="Datenquelle" value={stage.data_source} mono />
|
||||||
|
<Field label="Beispiel" value={stage.example} />
|
||||||
|
</dl>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{!last && <div className="flex justify-center text-gray-300 dark:text-gray-600 text-xs leading-none py-0.5">↓</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<dt className="text-[10px] uppercase tracking-wide text-gray-400">{label}</dt>
|
||||||
|
<dd className={`text-gray-600 dark:text-gray-300 ${mono ? 'font-mono text-[11px]' : ''}`}>{value}</dd>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renders `inline code` (single backticks) as <code> — the norm-matching bullets
|
||||||
|
// use backticks for function/identifier names.
|
||||||
|
function inlineCode(text: string): string {
|
||||||
|
const escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
return escaped.replace(/`([^`]+)`/g, '<code class="text-[11px] bg-gray-100 dark:bg-gray-700 rounded px-1">$1</code>')
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import IACEFlowFAB from './[projectId]/_components/IACEFlowFAB'
|
|||||||
|
|
||||||
const IACE_NAV_ITEMS = [
|
const IACE_NAV_ITEMS = [
|
||||||
{ id: 'overview', label: 'Uebersicht', href: '', icon: 'grid' },
|
{ id: 'overview', label: 'Uebersicht', href: '', icon: 'grid' },
|
||||||
|
{ id: 'architektur', label: 'Architektur & Datenfluss', href: '/architektur', icon: 'activity' },
|
||||||
{ id: 'order', label: 'Auftrag', href: '/order', icon: 'briefcase' },
|
{ id: 'order', label: 'Auftrag', href: '/order', icon: 'briefcase' },
|
||||||
{ id: 'interview', label: 'Grenzen & Verwendung', href: '/interview', icon: 'chat' },
|
{ id: 'interview', label: 'Grenzen & Verwendung', href: '/interview', icon: 'chat' },
|
||||||
{ id: 'operational-states', label: 'Betriebszustaende', href: '/operational-states', icon: 'activity' },
|
{ id: 'operational-states', label: 'Betriebszustaende', href: '/operational-states', icon: 'activity' },
|
||||||
|
|||||||
@@ -58,3 +58,11 @@ func (h *IACEHandler) GetRiskDataSources(c *gin.Context) {
|
|||||||
"evidence": iace.AllRiskEvidence(),
|
"evidence": iace.AllRiskEvidence(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetArchitecture handles GET /architecture.
|
||||||
|
// Data-driven self-description of the IACE engine (pipeline stages, libraries
|
||||||
|
// with LIVE counts, data sources + licenses, norm-matching logic) for the
|
||||||
|
// "Architektur & Datenfluss" auditability tab.
|
||||||
|
func (h *IACEHandler) GetArchitecture(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, iace.BuildArchitecture())
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) {
|
|||||||
iaceRoutes.GET("/projects/:id/risk-summary", h.GetRiskSummary)
|
iaceRoutes.GET("/projects/:id/risk-summary", h.GetRiskSummary)
|
||||||
iaceRoutes.GET("/projects/:id/risk-matrix", h.GetRiskMatrix)
|
iaceRoutes.GET("/projects/:id/risk-matrix", h.GetRiskMatrix)
|
||||||
iaceRoutes.GET("/risk-data-sources", h.GetRiskDataSources)
|
iaceRoutes.GET("/risk-data-sources", h.GetRiskDataSources)
|
||||||
|
iaceRoutes.GET("/architecture", h.GetArchitecture)
|
||||||
iaceRoutes.GET("/projects/:id/suggested-norms", h.SuggestProjectNorms)
|
iaceRoutes.GET("/projects/:id/suggested-norms", h.SuggestProjectNorms)
|
||||||
iaceRoutes.POST("/projects/:id/hazards/:hid/reassess", h.ReassessRisk)
|
iaceRoutes.POST("/projects/:id/hazards/:hid/reassess", h.ReassessRisk)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package iace
|
||||||
|
|
||||||
|
// Data-driven self-description of the IACE engine for the "Architektur &
|
||||||
|
// Datenfluss" explainer. Counts are LIVE (derived from the running engine) so
|
||||||
|
// the diagram can never drift from reality; the stage/source prose is curated.
|
||||||
|
// Purpose: let an auditor see WHERE each datum comes from and HOW a risk
|
||||||
|
// assessment is reached — every gate, library and data source, in order.
|
||||||
|
|
||||||
|
// ArchStage is one step of the deterministic pipeline.
|
||||||
|
type ArchStage struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
Input string `json:"input"`
|
||||||
|
Logic string `json:"logic"`
|
||||||
|
DataSource string `json:"data_source"` // code/library it draws from
|
||||||
|
Example string `json:"example"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchLibrary is one knowledge base with a LIVE entry count.
|
||||||
|
type ArchLibrary struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
SourceFile string `json:"source_file"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchDataSource is an external statistic/standard with its license + status.
|
||||||
|
type ArchDataSource struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
License string `json:"license"`
|
||||||
|
Usage string `json:"usage"`
|
||||||
|
Status string `json:"status"` // "verwendet" | "ausgeschlossen"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Architecture is the full self-description returned by GET /iace/architecture.
|
||||||
|
type Architecture struct {
|
||||||
|
Stages []ArchStage `json:"stages"`
|
||||||
|
Libraries []ArchLibrary `json:"libraries"`
|
||||||
|
DataSources []ArchDataSource `json:"data_sources"`
|
||||||
|
NormMatching []string `json:"norm_matching"`
|
||||||
|
Evidence []RiskEvidence `json:"evidence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// distinctDomainGates counts the distinct dom_* capability tags the engine gates on.
|
||||||
|
func distinctDomainGates() int {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, tag := range domainGateTerms {
|
||||||
|
seen[tag] = true
|
||||||
|
}
|
||||||
|
return len(seen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildArchitecture assembles the engine self-description with live counts.
|
||||||
|
func BuildArchitecture() Architecture {
|
||||||
|
return Architecture{
|
||||||
|
Stages: []ArchStage{
|
||||||
|
{
|
||||||
|
ID: "grenzen", Title: "1 · Grenzen-Formular",
|
||||||
|
Summary: "Maschinenbeschreibung nach EN ISO 12100 (Verwendungs-, räumliche, zeitliche Grenzen).",
|
||||||
|
Input: "17 Felder: Beschreibung, Verwendung, Fehlanwendung, Schnittstellen (elektrisch/mechanisch/pneumatisch-hydraulisch), Umgebung, Personen …",
|
||||||
|
Logic: "Alle Felder werden zu einer Narrative zusammengeführt (kein Whitelist — jedes Feld ist eine potenzielle Gefährdungsquelle).",
|
||||||
|
DataSource: "project.metadata.limits_form",
|
||||||
|
Example: "„Hubantrieb über Kette … 230 V … keine pneumatischen Schnittstellen.“",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "parse", Title: "2 · ParseNarrative",
|
||||||
|
Summary: "Deterministische Extraktion von Komponenten, Energiequellen, Domänen-Tags und Negationen.",
|
||||||
|
Input: "Narrative-Text + Maschinentyp",
|
||||||
|
Logic: "Keyword-Wörterbuch (Substring, umlaut-normalisiert) → Komponenten + Energie + dom_*-Tags. Negation („keine Pneumatik“) ⇒ Komponente als verneint markiert, liefert KEINE Tags.",
|
||||||
|
DataSource: "keyword_dictionary.go",
|
||||||
|
Example: "„Kette“→Komponente, „230 V“→electrical_energy, „Presse“→dom_press.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "match", Title: "3 · Pattern-Engine (Gates)",
|
||||||
|
Summary: "Jedes Gefährdungsmuster wird gegen die Maschine geprüft — harte UND-Gates.",
|
||||||
|
Input: "Komponenten-Tags, Energie-Tags, Lebensphasen, Maschinentyp, dom_*-Tags",
|
||||||
|
Logic: "patternMatches: MachineType ∧ Required-Component-Tags ∧ Required-Energy-Tags ∧ Lifecycle ∧ Operational-States. Capability-Domain-Gates (dom_*) verhindern Cross-Domänen-Leaks (z. B. Schwimmbad-Muster feuert nicht für eine Presse). Default-open, wenn ein Gate-Input leer ist.",
|
||||||
|
DataSource: "pattern_engine.go + pattern_domain_gates.go + hazard_patterns_*.go",
|
||||||
|
Example: "Presse-Muster feuert nur, wenn machine_type∈Presse-Familie UND high_force-Tag vorhanden.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "relevance", Title: "4 · Relevanz-Backstop",
|
||||||
|
Summary: "Generischer Filter gegen Rest-Leaks ungegateter Muster.",
|
||||||
|
Input: "Gefeuertes Muster + Narrative + Komponenten-Namen",
|
||||||
|
Logic: "IsPatternRelevant: Token-Grenzen + Stoppwort-Liste — ein Muster wird verworfen, wenn sein spezifischer Anker nicht in der Narrative vorkommt.",
|
||||||
|
DataSource: "pattern_relevance.go",
|
||||||
|
Example: "Verwirft „Bandsäge“-Hazard, wenn die Narrative keine Säge nennt.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "caps", Title: "5 · Kategorie-Caps",
|
||||||
|
Summary: "Begrenzung der Gefährdungen je Kategorie (skaliert mit Komponentenzahl).",
|
||||||
|
Input: "Gefeuerte Muster je Gefährdungskategorie",
|
||||||
|
Logic: "categoryHazardCap: pro Kategorie ein Maximum (verhindert Über-Flutung); Dedupe über Kategorie × Zone.",
|
||||||
|
DataSource: "iace_handler_init.go",
|
||||||
|
Example: "max. N mechanical_hazard-Gefährdungen je Projekt.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "hazards", Title: "6 · Gefährdungen",
|
||||||
|
Summary: "Die erzeugten Gefährdungen (Szenario, Auslöser, Schaden, Zone, betroffene Person).",
|
||||||
|
Input: "Überlebende Muster + zugeordnete Komponente",
|
||||||
|
Logic: "Pro Muster: Szenario/Trigger/Harm/Zone aus dem Muster; Komponentenzuordnung tag-basiert (pickComponentForPattern).",
|
||||||
|
DataSource: "iace_hazards (DB)",
|
||||||
|
Example: "„Quetschen im Werkzeugeinbauraum zwischen Ober- und Unterwerkzeug.“",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "measures", Title: "7 · Maßnahmen",
|
||||||
|
Summary: "Schutzmaßnahmen je Gefährdung — kategorie-gefiltert, KEINE generischen Defaults.",
|
||||||
|
Input: "Gefährdung + musterspezifische Suggested-Measure-IDs",
|
||||||
|
Logic: "Nur Maßnahmen, deren Kategorie zur Gefährdung passt (isCategoryCompatible). Ohne passende Maßnahme ⇒ 0 Maßnahmen + Coverage-Gap (ehrlich, statt Unsinn).",
|
||||||
|
DataSource: "measures_library*.go",
|
||||||
|
Example: "Sharp-edge-Gefährdung ⇒ keine „Rotation vermeiden“-Maßnahme.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "risk", Title: "8 · Risiko (S/F/W/P + Konfidenz)",
|
||||||
|
Summary: "Konfidenz-bewusste Risikoschätzung je Gefährdung — als Bereich, nicht Punktwert.",
|
||||||
|
Input: "Gefährdungskategorie + Szenario (Kontaktart) + Lebensphasen",
|
||||||
|
Logic: "EstimateSeverity/Frequency/ProbabilityW/AvoidabilityP → R = S×(F+W+P), Band + Bereich (±1 je validierter Genauigkeit) + Konfidenz (Verletzungsmechanismus eindeutig?). W verankert am ESAW-Kontaktmodus-Ranking; eigenes Modell, KEINE Norm-Tabelle.",
|
||||||
|
DataSource: "risk_estimation.go + risk_data_sources.go (ESAW, CC BY 4.0)",
|
||||||
|
Example: "Elektrischer Schlag: R≈32 (Bereich 21–45, mittel–kritisch), Konfidenz hoch.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "norms", Title: "9 · Normen (A/B/C + Familien-Matching)",
|
||||||
|
Summary: "Passende Normen je Maschinentyp und Gefährdung; DIN/ISO/OSHA-Vokabular versöhnt.",
|
||||||
|
Input: "Maschinentyp + Gefährdungskategorien + Tags",
|
||||||
|
Logic: "SuggestNorms: C-Normen exakt per Maschinentyp-FAMILIE (canonicalMachineType: welding_machine→welding); B-Normen per Gefährdungskategorie/Tags; A-Normen gelten immer. Normen werden nur referenziert, Tabellen nie reproduziert.",
|
||||||
|
DataSource: "norms_engine.go + machine_type_families.go + norms_library*.go",
|
||||||
|
Example: "Schweißanlage ⇒ EN 60974-x (Lichtbogenschweißen), obwohl Norm auf „welding_machine“ getaggt.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "matrix", Title: "10 · Risiko-Matrix / GT-Benchmark",
|
||||||
|
Summary: "Projektweite Risiko-Matrix (Schwere × Wahrscheinlichkeit) und Abgleich gegen Experten-Ground-Truth.",
|
||||||
|
Input: "Alle Gefährdungen + (optional) GT-Projekt",
|
||||||
|
Logic: "BuildRiskMatrix aggregiert je Zelle; Benchmark vergleicht Tool-S/F/W/P + Fine-Kinney gegen Fachmann-GT (Übereinstimmung within±1, Rang-Konkordanz).",
|
||||||
|
DataSource: "risk_matrix.go + risk_benchmark.go",
|
||||||
|
Example: "Kistenhub vs. eigene GT: S±1 94 %, Ranking 86 %.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Libraries: []ArchLibrary{
|
||||||
|
{Name: "Hazard-Pattern-Bibliothek", Count: len(AllPatterns()), SourceFile: "hazard_patterns_*.go", Description: "Gefährdungsmuster mit Gates (MachineType/Tags/Energy/Lifecycle) + Szenario/Trigger/Harm/Zone."},
|
||||||
|
{Name: "Maßnahmen-Bibliothek", Count: len(GetProtectiveMeasureLibrary()), SourceFile: "measures_library*.go", Description: "Schutzmaßnahmen mit Reduktionstyp + Norm-Referenzen, kategorie-gefiltert."},
|
||||||
|
{Name: "Normen-Bibliothek (A/B/C)", Count: len(collectAllNorms()), SourceFile: "norms_library*.go", Description: "A-/B-/C-Normen mit Maschinentypen, Gefährdungskategorien und Tags."},
|
||||||
|
{Name: "Komponenten-Bibliothek", Count: len(GetComponentLibrary()), SourceFile: "component_library.go", Description: "Bauteiltypen mit Capability-Tags für das Pattern-Gating."},
|
||||||
|
{Name: "Energiequellen", Count: len(GetEnergySources()), SourceFile: "component_library.go", Description: "Energiearten (elektrisch/pneumatisch/hydraulisch …) für Energie-Gates."},
|
||||||
|
{Name: "Maschinentyp-Vokabular", Count: len(MachineTypeVocabulary()), SourceFile: "machine_types.go", Description: "Kanonische Dropdown-Maschinentypen, auf die Patterns gaten."},
|
||||||
|
{Name: "Domänen-Capability-Gates", Count: distinctDomainGates(), SourceFile: "pattern_domain_gates.go", Description: "dom_*-Tags, die domänenspezifische Muster auf ihre echte Maschine begrenzen (Leak-Schutz)."},
|
||||||
|
{Name: "Kontaktmodus-Tiers", Count: len(contactModeTable), SourceFile: "risk_estimation.go", Description: "Verletzungsmechanismen mit W/P/S-Tiers (ESAW-verankert, GT-kalibriert)."},
|
||||||
|
{Name: "Kontaktmodus-Evidenz", Count: len(contactModeEvidence), SourceFile: "risk_data_sources.go", Description: "Belegte öffentliche Statistik-Quoten (ESAW) als Zitat-/Audit-Schicht."},
|
||||||
|
},
|
||||||
|
DataSources: []ArchDataSource{
|
||||||
|
{Name: "Eurostat ESAW (Kontaktmodus-Unfallstatistik)", License: "CC BY 4.0", Usage: "Anker für Wahrscheinlichkeits-Tiers (W) + zitierbare Quoten", Status: "verwendet"},
|
||||||
|
{Name: "US BLS / OSHA (Arbeitsunfälle)", License: "Public Domain", Usage: "Ergänzende Häufigkeits-/Schwere-Anker + OSHA-Maßnahmen", Status: "verwendet"},
|
||||||
|
{Name: "UK HSE (RIDDOR)", License: "Open Government Licence v3", Usage: "Zulässige Ergänzung (Attribution)", Status: "verwendet"},
|
||||||
|
{Name: "DGUV-Statistik", License: "nur redaktionell, keine Bearbeitung", Usage: "—", Status: "ausgeschlossen"},
|
||||||
|
{Name: "DIN/Beuth/ISO/IEC Risikograph-Tabellen", License: "urheberrechtlich", Usage: "Nur als Referenz genannt, NIE reproduziert/re-implementiert", Status: "ausgeschlossen"},
|
||||||
|
},
|
||||||
|
NormMatching: []string{
|
||||||
|
"C-Normen (maschinenspezifisch): Match nur über die kanonische Maschinentyp-FAMILIE — `canonicalMachineType` faltet das feingranulare Normen-Vokabular (455 Keys: welding_machine, band_saw, mobile_crane …) auf die 68 Dropdown-Keys. Ohne Familien-Match wird die C-Norm verworfen (kein Tag/Kategorie-Fallback → keine Fremd-Domänen-Normen).",
|
||||||
|
"B-Normen (gefährdungsspezifisch): Match über Gefährdungskategorie und Komponenten-/Energie-Tags.",
|
||||||
|
"A-Normen (Grundnormen): gelten immer (z. B. EN ISO 12100).",
|
||||||
|
"DIN/ISO/OSHA-Versöhnung: Normen tragen teils OSHA-/ISO-/DIN-nahe Maschinen-Keys; die Familien-Faltung sorgt dafür, dass z. B. eine „welding_machine“-Norm für eine „welding“-Maschine matched.",
|
||||||
|
"Lizenz-Leitplanke: Norm-Tabellen/Risikographen werden NIE reproduziert — nur Norm-Referenzen ausgegeben.",
|
||||||
|
},
|
||||||
|
Evidence: AllRiskEvidence(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package iace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuildArchitecture is the data-driven engine self-description rendered in the
|
||||||
|
// "Architektur & Datenfluss" auditability tab. These tests pin its shape and,
|
||||||
|
// crucially, that the library counts are LIVE (non-zero, drawn from the running
|
||||||
|
// engine) — a zero count would mean the diagram silently drifted from reality.
|
||||||
|
|
||||||
|
func TestBuildArchitecture_Shape(t *testing.T) {
|
||||||
|
a := BuildArchitecture()
|
||||||
|
|
||||||
|
if len(a.Stages) != 10 {
|
||||||
|
t.Errorf("expected 10 pipeline stages, got %d", len(a.Stages))
|
||||||
|
}
|
||||||
|
// Stage order is the audit narrative — first is the limits form, last the matrix.
|
||||||
|
if len(a.Stages) > 0 && a.Stages[0].ID != "grenzen" {
|
||||||
|
t.Errorf("first stage should be grenzen, got %q", a.Stages[0].ID)
|
||||||
|
}
|
||||||
|
if last := a.Stages[len(a.Stages)-1]; last.ID != "matrix" {
|
||||||
|
t.Errorf("last stage should be matrix, got %q", last.ID)
|
||||||
|
}
|
||||||
|
for _, s := range a.Stages {
|
||||||
|
if s.Title == "" || s.Summary == "" || s.Logic == "" || s.DataSource == "" {
|
||||||
|
t.Errorf("stage %q has empty required prose: %+v", s.ID, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a.NormMatching) == 0 {
|
||||||
|
t.Error("norm_matching explanation must not be empty")
|
||||||
|
}
|
||||||
|
if len(a.Evidence) == 0 {
|
||||||
|
t.Error("evidence (ESAW citations) must not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArchitecture_LiveLibraryCounts(t *testing.T) {
|
||||||
|
a := BuildArchitecture()
|
||||||
|
|
||||||
|
if len(a.Libraries) == 0 {
|
||||||
|
t.Fatal("no libraries reported")
|
||||||
|
}
|
||||||
|
for _, l := range a.Libraries {
|
||||||
|
if l.Name == "" || l.SourceFile == "" {
|
||||||
|
t.Errorf("library missing name/source: %+v", l)
|
||||||
|
}
|
||||||
|
if l.Count <= 0 {
|
||||||
|
t.Errorf("library %q has non-live count %d (expected >0 from running engine)", l.Name, l.Count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildArchitecture_DataSourcesIncludeExclusions(t *testing.T) {
|
||||||
|
a := BuildArchitecture()
|
||||||
|
|
||||||
|
var hasUsed, hasExcluded bool
|
||||||
|
for _, d := range a.DataSources {
|
||||||
|
switch d.Status {
|
||||||
|
case "verwendet":
|
||||||
|
hasUsed = true
|
||||||
|
case "ausgeschlossen":
|
||||||
|
hasExcluded = true
|
||||||
|
default:
|
||||||
|
t.Errorf("data source %q has unexpected status %q", d.Name, d.Status)
|
||||||
|
}
|
||||||
|
if d.License == "" {
|
||||||
|
t.Errorf("data source %q missing license", d.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasUsed {
|
||||||
|
t.Error("expected at least one used data source")
|
||||||
|
}
|
||||||
|
// The copyright guardrail is auditable only if the EXCLUDED norm tables are
|
||||||
|
// shown as deliberately not-reproduced — not silently omitted.
|
||||||
|
if !hasExcluded {
|
||||||
|
t.Error("expected DIN/Beuth norm tables to appear as an explicit exclusion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The licensing guardrail must be spelled out in the norm-matching prose:
|
||||||
|
// norm tables are referenced, never reproduced.
|
||||||
|
joined := strings.ToLower(strings.Join(a.NormMatching, " "))
|
||||||
|
if !strings.Contains(joined, "reproduziert") {
|
||||||
|
t.Error("norm-matching prose should state norm tables are never reproduced")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ type NormSuggestionResult struct {
|
|||||||
// identified hazard categories, and component/energy tags.
|
// identified hazard categories, and component/energy tags.
|
||||||
// A-norms are always included (they apply universally). B/C norms are matched
|
// A-norms are always included (they apply universally). B/C norms are matched
|
||||||
// by machine type (confidence 0.9), hazard category (0.8), or tag (0.7).
|
// by machine type (confidence 0.9), hazard category (0.8), or tag (0.7).
|
||||||
func SuggestNorms(machineType string, hazardCategories []string, tags []string) *NormSuggestionResult {
|
// collectAllNorms aggregates every A/B/C norm source into one slice. Single
|
||||||
|
// source of truth used by SuggestNorms and the architecture self-description.
|
||||||
|
func collectAllNorms() []NormReference {
|
||||||
allNorms := GetNormsLibrary()
|
allNorms := GetNormsLibrary()
|
||||||
allNorms = append(allNorms, GetExtendedB2Norms()...)
|
allNorms = append(allNorms, GetExtendedB2Norms()...)
|
||||||
allNorms = append(allNorms, GetCNormsLibrary()...)
|
allNorms = append(allNorms, GetCNormsLibrary()...)
|
||||||
@@ -48,6 +50,11 @@ func SuggestNorms(machineType string, hazardCategories []string, tags []string)
|
|||||||
allNorms = append(allNorms, GetWave3dExtCNorms()...)
|
allNorms = append(allNorms, GetWave3dExtCNorms()...)
|
||||||
allNorms = append(allNorms, GetWave3dHvacCNorms()...)
|
allNorms = append(allNorms, GetWave3dHvacCNorms()...)
|
||||||
allNorms = append(allNorms, GetFinalCNorms()...)
|
allNorms = append(allNorms, GetFinalCNorms()...)
|
||||||
|
return allNorms
|
||||||
|
}
|
||||||
|
|
||||||
|
func SuggestNorms(machineType string, hazardCategories []string, tags []string) *NormSuggestionResult {
|
||||||
|
allNorms := collectAllNorms()
|
||||||
|
|
||||||
// Build lookup sets for efficient matching
|
// Build lookup sets for efficient matching
|
||||||
hazardSet := toSet(hazardCategories)
|
hazardSet := toSet(hazardCategories)
|
||||||
|
|||||||
Reference in New Issue
Block a user