diff --git a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts index 6f3baec..0a1c934 100644 --- a/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts +++ b/admin-compliance/app/api/sdk/drafting-engine/draft/draft-helpers-v2.ts @@ -240,7 +240,7 @@ export async function handleV2Draft(body: Record): Promise): Promise } ) { try { + const { id } = await params const tenantId = getTenantId(request) const response = await fetch( - `${BACKEND_URL}/api/compliance/einwilligungen/consents/${params.id}/history`, + `${BACKEND_URL}/api/compliance/einwilligungen/consents/${id}/history`, { method: 'GET', headers: { diff --git a/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts index e606372..b08af28 100644 --- a/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/maximizer/[[...path]]/route.ts @@ -39,14 +39,14 @@ async function proxy(request: NextRequest, params: { path?: string[] }, method: } } -export async function GET(request: NextRequest, { params }: { params: { path?: string[] } }) { - return proxy(request, params, 'GET') +export async function GET(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxy(request, await params, 'GET') } -export async function POST(request: NextRequest, { params }: { params: { path?: string[] } }) { - return proxy(request, params, 'POST') +export async function POST(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxy(request, await params, 'POST') } -export async function DELETE(request: NextRequest, { params }: { params: { path?: string[] } }) { - return proxy(request, params, 'DELETE') +export async function DELETE(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { + return proxy(request, await params, 'DELETE') } diff --git a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts b/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts index ddbb733..20162df 100644 --- a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts +++ b/admin-compliance/app/api/sdk/v1/ucca/decision-tree/[[...path]]/route.ts @@ -6,7 +6,7 @@ const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78 /** * Proxy: /api/sdk/v1/ucca/decision-tree/... → Go Backend /sdk/v1/ucca/decision-tree/... */ -async function proxyRequest(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) { +async function proxyRequest(request: NextRequest, { params }: { params: Promise<{ path?: string[] }> }) { const { path } = await params const subPath = path ? path.join('/') : '' const search = request.nextUrl.search || '' diff --git a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/route.ts b/admin-compliance/app/api/sdk/v1/ucca/decision-tree/route.ts deleted file mode 100644 index d8b1c33..0000000 --- a/admin-compliance/app/api/sdk/v1/ucca/decision-tree/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' - -const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090' -const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' - -/** - * Proxy: GET /api/sdk/v1/ucca/decision-tree → Go Backend GET /sdk/v1/ucca/decision-tree - * Returns the decision tree definition (questions, structure) - */ -export async function GET(request: NextRequest) { - const tenantID = request.headers.get('X-Tenant-ID') || DEFAULT_TENANT - - try { - const response = await fetch(`${SDK_URL}/sdk/v1/ucca/decision-tree`, { - headers: { 'X-Tenant-ID': tenantID }, - }) - - if (!response.ok) { - const errorText = await response.text() - console.error('Decision tree GET error:', errorText) - return NextResponse.json( - { error: 'Backend error', details: errorText }, - { status: response.status } - ) - } - - const data = await response.json() - return NextResponse.json(data) - } catch (error) { - console.error('Decision tree proxy error:', error) - return NextResponse.json( - { error: 'Failed to connect to AI compliance backend' }, - { status: 503 } - ) - } -} diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 7bc513b..94adc36 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -62,6 +62,14 @@ export default function ControlLibraryPage() { initial={{ ...EMPTY_CONTROL, ...state.selectedControl, + scope: { + platforms: state.selectedControl.scope?.platforms ?? [], + components: state.selectedControl.scope?.components ?? [], + data_classes: state.selectedControl.scope?.data_classes ?? [], + }, + target_audience: Array.isArray(state.selectedControl.target_audience) + ? state.selectedControl.target_audience.join(', ') + : state.selectedControl.target_audience, risk_score: state.selectedControl.risk_score, implementation_effort: state.selectedControl.implementation_effort, open_anchors: state.selectedControl.open_anchors.length > 0 @@ -69,7 +77,9 @@ export default function ControlLibraryPage() { : [{ framework: '', ref: '', url: '' }], requirements: state.selectedControl.requirements.length > 0 ? state.selectedControl.requirements : [''], test_procedure: state.selectedControl.test_procedure.length > 0 ? state.selectedControl.test_procedure : [''], - evidence: state.selectedControl.evidence.length > 0 ? state.selectedControl.evidence : [{ type: '', description: '' }], + evidence: state.selectedControl.evidence.length > 0 + ? state.selectedControl.evidence.map(e => typeof e === 'string' ? { type: '', description: e } : e) + : [{ type: '', description: '' }], }} onSave={handleUpdate} onCancel={() => state.setMode('detail')} diff --git a/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx b/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx index 6db00e1..2648b20 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/_components/VariantPanel.tsx @@ -18,12 +18,91 @@ interface VariantGapResponse { gap: { additional_hazards: number; additional_measures: number; categories_affected: string[] } } +interface BaseProjectSummary { + hazard_count: number + component_count: number + mitigation_count: number + norms_count: number +} + interface Props { projectId: string parentProjectId?: string | null parentProjectName?: string } +function VariantBanner({ projectId, parentProjectId, parentProjectName }: { projectId: string; parentProjectId: string; parentProjectName?: string }) { + const [baseSummary, setBaseSummary] = useState(null) + + useEffect(() => { + async function loadBase() { + try { + const [projRes, riskRes] = await Promise.all([ + fetch(`/api/sdk/v1/iace/projects/${parentProjectId}`), + fetch(`/api/sdk/v1/iace/projects/${parentProjectId}/risk-summary`), + ]) + const proj = projRes.ok ? await projRes.json() : null + const risk = riskRes.ok ? await riskRes.json() : null + const rs = risk?.risk_summary || risk || {} + setBaseSummary({ + hazard_count: rs.total_hazards || rs.total || 0, + component_count: proj?.components?.length || 0, + mitigation_count: rs.total_mitigations || 0, + norms_count: 0, + }) + } catch { /* ignore */ } + } + loadBase() + }, [parentProjectId]) + + return ( +
+
+
+ + + +
+
+

Variante

+

+ Diese Seite zeigt nur die varianten-spezifischen Gefaehrdungen und Massnahmen. + Die Basis-Risikobeurteilung liegt im Eltern-Projekt. +

+
+ + {parentProjectName || 'Basis-Projekt'} + + + + +
+ {baseSummary && ( +
+

Basis-Projekt Zusammenfassung

+
+
+
{baseSummary.hazard_count}
+
Gefaehrdungen
+
+
+
{baseSummary.mitigation_count}
+
Massnahmen
+
+
+
{baseSummary.component_count}
+
Komponenten
+
+
+
+ )} +
+ ) +} + export function VariantPanel({ projectId, parentProjectId, parentProjectName }: Props) { const [variants, setVariants] = useState([]) const [gapMap, setGapMap] = useState>({}) @@ -95,34 +174,9 @@ export function VariantPanel({ projectId, parentProjectId, parentProjectName }: } } - // If this project IS a variant, show link to base project + // If this project IS a variant, show link to base project + base stats if (parentProjectId) { - return ( -
-
-
- - - -
-
-

Variante

-

- Dieses Projekt ist eine Variante des Basis-Projekts -

-
- - {parentProjectName || 'Basis-Projekt'} - - - - -
-
- ) + return } if (loading) return null diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RegulatoryHintsPanel.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RegulatoryHintsPanel.tsx new file mode 100644 index 0000000..0386e68 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RegulatoryHintsPanel.tsx @@ -0,0 +1,123 @@ +'use client' + +import React, { useState, useCallback } from 'react' + +interface RegulatoryHint { + regulation_id: string + regulation_short: string + category: string + text: string + pages?: number[] + source_url?: string + score: number +} + +interface Props { + projectId: string + hazardId: string + hazardName: string +} + +function categoryBadge(cat: string): string { + if (cat === 'trbs') return 'bg-orange-100 text-orange-800' + if (cat === 'trgs') return 'bg-red-100 text-red-800' + if (cat === 'asr') return 'bg-teal-100 text-teal-800' + if (cat === 'osha' || cat.startsWith('ce_')) return 'bg-blue-100 text-blue-800' + return 'bg-gray-100 text-gray-700' +} + +export function RegulatoryHintsPanel({ projectId, hazardId, hazardName }: Props) { + const [hints, setHints] = useState([]) + const [loading, setLoading] = useState(false) + const [loaded, setLoaded] = useState(false) + const [error, setError] = useState(null) + + const loadHints = useCallback(async () => { + setLoading(true) + setError(null) + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards/${hazardId}/regulatory-hints`) + if (!res.ok) { + setError('Hinweise konnten nicht geladen werden') + return + } + const data = await res.json() + setHints(data.hints || []) + } catch { + setError('Verbindung zum RAG-Service fehlgeschlagen') + } finally { + setLoading(false) + setLoaded(true) + } + }, [projectId, hazardId]) + + if (!loaded) { + return ( + + ) + } + + if (error) { + return

{error}

+ } + + if (hints.length === 0) { + return

Keine regulatorischen Hinweise gefunden

+ } + + return ( +
+

+ Regulatorische Hinweise ({hints.length}) +

+ {hints.map((hint, i) => ( +
+
+ + {hint.regulation_short || hint.regulation_id} + + {hint.pages && hint.pages.length > 0 && ( + S. {hint.pages.join(', ')} + )} + {(hint.score * 100).toFixed(0)}% Relevanz +
+

{hint.text}

+ {hint.source_url && ( + + Quelle + + + + + )} +
+ ))} +
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskAssessmentTable.tsx b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskAssessmentTable.tsx index 90db017..6f0cb95 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskAssessmentTable.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/RiskAssessmentTable.tsx @@ -1,9 +1,10 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { Hazard, CATEGORY_LABELS, getRiskColor, getRiskLevelLabel, getRiskLevelISO, } from './types' +import { RegulatoryHintsPanel } from './RegulatoryHintsPanel' interface RiskAssessmentTableProps { projectId: string @@ -81,6 +82,7 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, const [edits, setEdits] = useState>({}) const [saving, setSaving] = useState(null) const [normsByCategory, setNormsByCategory] = useState>({}) + const [expandedHazard, setExpandedHazard] = useState(null) // Fetch norms library and build category→norm-numbers map useEffect(() => { @@ -123,7 +125,7 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, for (const h of hazards) { if (!edits[h.id]) { // Read from risk_assessment if available (enriched response), fallback to hazard fields - const ra = (h as Record).risk_assessment as Record | null + const ra = (h as unknown as Record).risk_assessment as Record | null init[h.id] = { severity: ra?.severity || h.severity || 3, exposure: ra?.exposure || h.exposure || 3, @@ -190,7 +192,7 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, Gefaehrdung Erstbewertung Nach Massnahmen (editierbar) - SIL / PL + SIL / PL * Status {/* Column header */} @@ -220,7 +222,7 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, {paged.map(h => { - const ra = (h as Record).risk_assessment as Record | null + const ra = (h as unknown as Record).risk_assessment as Record | null const initS = ra?.severity || h.severity || 3 const initE = ra?.exposure || h.exposure || 3 const initP = ra?.probability || h.probability || 3 @@ -235,10 +237,13 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, const changed = e && (e.severity !== initS || e.exposure !== initE || e.probability !== initP) return ( - + + {/* Hazard info */} -
{h.name}
+ {h.component_name &&
{h.component_name}
} @@ -279,14 +284,16 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, )} - {/* SIL / PL */} + {/* SIL / PL (Vorab-Einschaetzung) */} - + {sil > 0 ? `SIL ${sil}` : '-'} - + PL {pl} @@ -325,6 +332,31 @@ export function RiskAssessmentTable({ projectId, hazards, onReassess, decisions, )} + {expandedHazard === h.id && ( + + + {/* Hazard details */} + {h.scenario &&

Szenario: {h.scenario}

} + {h.possible_harm &&

Moeglicher Schaden: {h.possible_harm}

} + {/* Match reasons (explainability) */} + {h.match_reasons && h.match_reasons.length > 0 && ( +
+ Erkannt weil:{' '} + {h.match_reasons + .filter(r => r.met) + .map((r, i) => ( + + {r.type === 'required_component_tag' ? 'Komponente' : r.type === 'required_energy_tag' ? 'Energie' : r.type === 'no_exclusion' ? 'Kein Ausschluss' : 'Lifecycle'}: {r.tag} + + ))} +
+ )} + {/* Regulatory hints */} + + + + )} +
) })} diff --git a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts index 6aa55f4..5eafdbe 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts +++ b/admin-compliance/app/sdk/iace/[projectId]/hazards/_components/types.ts @@ -19,9 +19,11 @@ export interface Hazard { affected_person: string possible_harm: string hazardous_zone: string + scenario?: string review_status: string created_at: string source?: string + match_reasons?: { type: string; tag: string; met: boolean }[] } export interface LibraryHazard { diff --git a/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx index 8c9f7a6..1d68291 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/interview/page.tsx @@ -24,6 +24,8 @@ export default function IACEInterviewPage() { const [projectData, setProjectData] = useState(null) const [loading, setLoading] = useState(true) const [saveStatus, setSaveStatus] = useState('idle') + const [initStatus, setInitStatus] = useState<'idle' | 'running' | 'done' | 'error'>('idle') + const [initResult, setInitResult] = useState<{ steps: { name: string; status: string; count: number; details?: string }[]; summary: Record } | null>(null) const saveTimerRef = useRef | null>(null) const latestFormRef = useRef(EMPTY_LIMITS_FORM) @@ -157,7 +159,34 @@ export default function IACEInterviewPage() { }} /> - {/* Navigation */} + {/* Initialization Result */} + {initResult && ( +
+

Initialisierung abgeschlossen

+
+ {Object.entries(initResult.summary).map(([key, val]) => ( +
+
{val}
+
{key}
+
+ ))} +
+
+ {initResult.steps.map((s, i) => ( +
+ + {s.status === 'done' ? '\u2713' : s.status === 'skipped' ? '\u25CB' : '\u2717'} + + {s.name} + {s.count > 0 && ({s.count})} + {s.details && — {s.details}} +
+ ))} +
+
+ )} + + {/* Navigation + Initialize */}
- +
+ + +
) diff --git a/admin-compliance/app/sdk/iace/[projectId]/mitigations/_components/MitigationHints.tsx b/admin-compliance/app/sdk/iace/[projectId]/mitigations/_components/MitigationHints.tsx new file mode 100644 index 0000000..23290dc --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/mitigations/_components/MitigationHints.tsx @@ -0,0 +1,69 @@ +'use client' + +import React, { useState, useCallback } from 'react' + +interface Hint { + regulation_id: string + regulation_short: string + category: string + text: string + pages?: number[] + source_url?: string + score: number +} + +function catBadge(cat: string): string { + if (cat === 'trbs') return 'bg-orange-100 text-orange-800' + if (cat === 'trgs') return 'bg-red-100 text-red-800' + if (cat === 'asr') return 'bg-teal-100 text-teal-800' + return 'bg-blue-100 text-blue-800' +} + +interface Props { + projectId: string + mitigationId: string +} + +export function MitigationHints({ projectId, mitigationId }: Props) { + const [hints, setHints] = useState([]) + const [loading, setLoading] = useState(false) + const [loaded, setLoaded] = useState(false) + + const load = useCallback(async () => { + setLoading(true) + try { + const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations/${mitigationId}/regulatory-hints`) + if (res.ok) { + const data = await res.json() + setHints(data.hints || []) + } + } catch { /* ignore */ } + finally { setLoading(false); setLoaded(true) } + }, [projectId, mitigationId]) + + if (!loaded) { + return ( + + ) + } + + if (hints.length === 0) return Keine Hinweise + + return ( +
+

Regulatorische Hinweise:

+ {hints.map((h, i) => ( +
+ + {h.regulation_short || h.regulation_id} + + {h.pages?.length ? S.{h.pages.join(',')} : null} +

{h.text}

+
+ ))} +
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx index 8b9b327..b9f3d60 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/mitigations/page.tsx @@ -8,6 +8,7 @@ import { MeasuresLibraryModal } from './_components/MeasuresLibraryModal' import { SuggestMeasuresModal } from './_components/SuggestMeasuresModal' import { MitigationForm } from './_components/MitigationForm' import { StatusBadge } from './_components/StatusBadge' +import { MitigationHints } from './_components/MitigationHints' import { ProtectiveMeasure } from './_components/types' import { useMitigations } from './_hooks/useMitigations' @@ -238,6 +239,7 @@ export default function MitigationsPage() { {m.description &&

{m.description}

} {category &&

Diese Massnahme gilt fuer alle Gefaehrdungen der Kategorie {category}.

} {refs?.length > 0 &&

Normen: {refs.join(', ')}

} + )} diff --git a/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx index 8f77838..4ff0eaf 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/norms/page.tsx @@ -35,6 +35,9 @@ export default function NormsPage() { {/* Suggested norms component — rendered expanded (not collapsed by default) */} + + {/* Document upload — own norms/specs/reports */} + ) } diff --git a/admin-compliance/app/sdk/iace/[projectId]/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/page.tsx index ad83eae..c86d30a 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/page.tsx @@ -348,6 +348,13 @@ export default function ProjectOverviewPage() { + {/* Safety Disclaimer */} +
+ Hinweis: Automatisch erkannte Gefaehrdungen, Massnahmen und SIL/PL-Einschaetzungen sind eine qualifizierte Ausgangsbasis. + Vollstaendigkeit und Angemessenheit muessen vom CE-Verantwortlichen des Herstellers validiert werden. + Dieses Tool ersetzt nicht die Pflichten des Herstellers nach EU 2023/1230 Art. 10. +
+ {/* Compliance Alerts */} diff --git a/admin-compliance/app/sdk/iace/[projectId]/tech-file/_components/ReportPrintView.tsx b/admin-compliance/app/sdk/iace/[projectId]/tech-file/_components/ReportPrintView.tsx index 75b8e2e..8f740da 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/tech-file/_components/ReportPrintView.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/tech-file/_components/ReportPrintView.tsx @@ -111,6 +111,14 @@ export function ReportPrintView({ data }: ReportPrintViewProps) { + {/* Disclaimer */} +
+ Hinweis: Dieses Dokument wurde mit BreakPilot ComplAI erstellt. Automatisch erkannte Gefaehrdungen + und Massnahmen sind eine qualifizierte Ausgangsbasis. Vollstaendigkeit und Angemessenheit muessen vom + CE-Verantwortlichen des Herstellers validiert werden. Dieses Tool ersetzt nicht die Pflichten des + Herstellers nach EU Maschinenverordnung 2023/1230 Art. 10. +
+ {/* 2. Inhaltsverzeichnis */}

Inhaltsverzeichnis

diff --git a/admin-compliance/app/sdk/iace/[projectId]/tech-file/page.tsx b/admin-compliance/app/sdk/iace/[projectId]/tech-file/page.tsx index 62f02a8..7a4102a 100644 --- a/admin-compliance/app/sdk/iace/[projectId]/tech-file/page.tsx +++ b/admin-compliance/app/sdk/iace/[projectId]/tech-file/page.tsx @@ -255,6 +255,10 @@ export default function TechFilePage() { Technische Dokumentation gemaess Maschinenverordnung Anhang IV. Generieren, pruefen und freigeben Sie alle erforderlichen Abschnitte.

+
+ Hinweis: Unterstuetzte Risikobeurteilung — automatisch erkannte Gefaehrdungen und Massnahmen sind eine qualifizierte Ausgangsbasis. + Vollstaendigkeit muss vom CE-Verantwortlichen validiert werden. Dieses Tool ersetzt nicht die Pflichten des Herstellers nach EU 2023/1230. +
{/* Risk Report Export (PDF + Excel) — always available */} diff --git a/admin-compliance/app/sdk/iace/library/_components/DokumenteTab.tsx b/admin-compliance/app/sdk/iace/library/_components/DokumenteTab.tsx new file mode 100644 index 0000000..452415d --- /dev/null +++ b/admin-compliance/app/sdk/iace/library/_components/DokumenteTab.tsx @@ -0,0 +1,191 @@ +'use client' + +import React, { useMemo, useState, useRef, useEffect } from 'react' +import { SearchInput, FilterDropdown, Pagination, ExternalLinkIcon } from './LibraryTable' + +export interface CEDocument { + regulation_id: string + name_de: string + name_en: string + category: string + source_url: string + source_org: string + chunk_count: number +} + +const PER_PAGE = 50 + +const CATEGORY_LABELS: Record = { + trbs: 'TRBS — Betriebssicherheit', + trgs: 'TRGS — Gefahrstoffe', + asr: 'ASR — Arbeitsstaetten', + osha: 'OSHA — US Occupational Safety', + ce_machinery: 'EU — Maschinenrecht', + ce_machinery_guidance: 'EU — Leitfaeden', + ce_electrical: 'EU — Niederspannung', + ce_emc: 'EU — EMV', + ce_radio: 'EU — Funkanlagen', + ce_ai: 'EU — KI-Verordnung', + ce_ai_safety: 'KI-Sicherheit', + ce_software_safety: 'Software-Sicherheit', + ce_software_security: 'Software-Security', + ce_software_weaknesses: 'Software-Schwachstellen', + ce_ot_cybersecurity: 'OT-Cybersecurity', + eu_recht: 'EU-Recht', + eu_datenschutz: 'EU-Datenschutz', + guidance: 'Guidance', +} + +function categoryLabel(cat: string): string { + return CATEGORY_LABELS[cat] || cat.replace(/_/g, ' ') +} + +function categoryColor(cat: string): string { + if (cat.startsWith('trbs')) return 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300' + if (cat.startsWith('trgs')) return 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300' + if (cat.startsWith('asr')) return 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300' + if (cat === 'osha') return 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300' + if (cat.startsWith('ce_')) return 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300' + if (cat.startsWith('eu_')) return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300' + return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300' +} + +interface Props { + documents: CEDocument[] + loading?: boolean +} + +export default function DokumenteTab({ documents, loading }: Props) { + const [search, setSearch] = useState('') + const [debounced, setDebounced] = useState('') + const [categoryFilter, setCategoryFilter] = useState('') + const [page, setPage] = useState(1) + const timer = useRef | null>(null) + + useEffect(() => { + timer.current = setTimeout(() => setDebounced(search), 300) + return () => { if (timer.current) clearTimeout(timer.current) } + }, [search]) + + useEffect(() => { setPage(1) }, [debounced, categoryFilter]) + + const categories = useMemo(() => { + const cats = new Set(documents.map((d) => d.category)) + const opts = [{ value: '', label: 'Alle Kategorien' }] + Array.from(cats).sort().forEach((c) => opts.push({ value: c, label: categoryLabel(c) })) + return opts + }, [documents]) + + const filtered = useMemo(() => { + const q = debounced.toLowerCase() + return documents.filter((d) => { + if (categoryFilter && d.category !== categoryFilter) return false + if (q) { + const hay = `${d.regulation_id} ${d.name_de} ${d.name_en} ${d.source_org}`.toLowerCase() + if (!hay.includes(q)) return false + } + return true + }) + }, [documents, debounced, categoryFilter]) + + const totalPages = Math.ceil(filtered.length / PER_PAGE) + const pageItems = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE) + + const totalChunks = filtered.reduce((sum, d) => sum + d.chunk_count, 0) + + if (loading) { + return ( +
+
+ Lade Dokumentenindex aus Qdrant... +
+ ) + } + + return ( +
+ {/* Stats bar */} +
+ {documents.length} Dokumente + | + {documents.reduce((s, d) => s + d.chunk_count, 0).toLocaleString()} Chunks im Vektorspeicher + | + Quellen: BAuA, OSHA, EUR-Lex, NIST, ENISA +
+ + {/* Filters */} +
+
+ +
+ + + {filtered.length !== documents.length && `${filtered.length} gefiltert | `}{totalChunks.toLocaleString()} Chunks + +
+ + {/* Table */} +
+ + + + {['Kennung', 'Bezeichnung', 'Kategorie', 'Quelle', 'Chunks'].map((h) => ( + + ))} + + + + {pageItems.map((d) => ( + + + + + + + + ))} + {pageItems.length === 0 && ( + + + + )} + +
{h}
+ {d.regulation_id} + +
+
{d.name_de || d.name_en || d.regulation_id}
+ {d.source_url && ( + e.stopPropagation()} + > + Quelle + + )} +
+
+ + {categoryLabel(d.category)} + + + {d.source_org || '-'} + + {d.chunk_count.toLocaleString()} +
+ {documents.length === 0 + ? 'Keine Dokumente im CE-Corpus gefunden. Qdrant-Verbindung pruefen.' + : 'Keine Dokumente fuer diesen Filter gefunden'} +
+
+ + +
+ ) +} diff --git a/admin-compliance/app/sdk/iace/library/page.tsx b/admin-compliance/app/sdk/iace/library/page.tsx index 8434746..a40d8f7 100644 --- a/admin-compliance/app/sdk/iace/library/page.tsx +++ b/admin-compliance/app/sdk/iace/library/page.tsx @@ -5,13 +5,15 @@ import Link from 'next/link' import NormenTab, { type Norm } from './_components/NormenTab' import PatternsTab, { type HazardPattern } from './_components/PatternsTab' import MeasuresTab, { type ProtectiveMeasure } from './_components/MeasuresTab' +import DokumenteTab, { type CEDocument } from './_components/DokumenteTab' -type TabId = 'normen' | 'patterns' | 'measures' +type TabId = 'normen' | 'patterns' | 'measures' | 'dokumente' const TABS: { id: TabId; label: string }[] = [ { id: 'normen', label: 'Normen' }, { id: 'patterns', label: 'Patterns' }, { id: 'measures', label: 'Massnahmen' }, + { id: 'dokumente', label: 'Dokumente' }, ] export default function IACELibraryPage() { @@ -19,6 +21,9 @@ export default function IACELibraryPage() { const [norms, setNorms] = useState([]) const [patterns, setPatterns] = useState([]) const [measures, setMeasures] = useState([]) + const [documents, setDocuments] = useState([]) + const [docsLoading, setDocsLoading] = useState(false) + const [docsFetched, setDocsFetched] = useState(false) const [loading, setLoading] = useState(true) useEffect(() => { @@ -50,10 +55,22 @@ export default function IACELibraryPage() { load() }, []) + // Lazy-load documents on tab switch + useEffect(() => { + if (activeTab !== 'dokumente' || docsFetched) return + setDocsLoading(true) + fetch('/api/sdk/v1/iace/ce-corpus-documents') + .then((r) => r.ok ? r.json() : null) + .then((data) => { if (data) setDocuments(data.documents || []) }) + .catch(() => {}) + .finally(() => { setDocsLoading(false); setDocsFetched(true) }) + }, [activeTab, docsFetched]) + const counts: Record = { normen: norms.length, patterns: patterns.length, measures: measures.length, + dokumente: documents.length, } if (loading) { @@ -116,6 +133,7 @@ export default function IACELibraryPage() { {activeTab === 'normen' && } {activeTab === 'patterns' && } {activeTab === 'measures' && } + {activeTab === 'dokumente' && }
) diff --git a/admin-compliance/e2e/specs/iace-project-tabs.spec.ts b/admin-compliance/e2e/specs/iace-project-tabs.spec.ts new file mode 100644 index 0000000..c5a4fd8 --- /dev/null +++ b/admin-compliance/e2e/specs/iace-project-tabs.spec.ts @@ -0,0 +1,78 @@ +import { test, expect } from '@playwright/test' + +const BASE = 'https://macmini:3007' +const PROJECT_ID = '50d16b08-d21c-450a-af62-222f1949acf9' // Kniehebelpresse + +test.describe('IACE Project — All Tabs', () => { + test.beforeEach(async ({ page }) => { + // Ignore SSL errors + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}`, { waitUntil: 'networkidle' }) + }) + + test('Overview page loads without error', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}`, { waitUntil: 'networkidle' }) + // Should not have "Application error" + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + // Should show project name + await expect(page.locator('text=Kniehebelpresse')).toBeVisible({ timeout: 10000 }) + }) + + test('Components tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/components`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Classification tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/classification`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Hazards tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/hazards`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Mitigations tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/mitigations`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Verification tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/verification`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Evidence tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/evidence`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Tech-File tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/tech-file`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Monitoring tab loads', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/monitoring`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + }) + + test('Interview page loads with 3 modes', async ({ page }) => { + await page.goto(`${BASE}/sdk/iace/${PROJECT_ID}/interview`, { waitUntil: 'networkidle' }) + const body = await page.textContent('body') + expect(body).not.toContain('Application error') + // Check 3 mode buttons exist + await expect(page.locator('text=Interview')).toBeVisible() + await expect(page.locator('text=Wizard')).toBeVisible() + await expect(page.locator('text=Formular')).toBeVisible() + }) +}) diff --git a/admin-compliance/lib/sdk/__tests__/types.test.ts b/admin-compliance/lib/sdk/__tests__/types.test.ts index b530c8f..916ab55 100644 --- a/admin-compliance/lib/sdk/__tests__/types.test.ts +++ b/admin-compliance/lib/sdk/__tests__/types.test.ts @@ -113,16 +113,25 @@ describe('getPreviousStep', () => { }) describe('getCompletionPercentage', () => { - const createMockState = (completedSteps: string[]): SDKState => ({ + const createMockState = (completedSteps: string[]) => ({ version: '1.0.0', + projectVersion: 1, lastModified: new Date(), tenantId: 'test', userId: 'test', subscription: 'PROFESSIONAL', + projectId: 'test-project', + projectInfo: null, + customerType: null, + companyProfile: null, + complianceScope: null, + sourcePolicy: null, currentPhase: 1, currentStep: 'use-case-assessment', completedSteps, checkpoints: {}, + importedDocuments: [], + gapAnalysis: null, useCases: [], activeUseCase: null, screening: null, @@ -143,9 +152,12 @@ describe('getCompletionPercentage', () => { consents: [], dsrConfig: null, escalationWorkflows: [], + iaceProjects: [], + ragCorpusStatus: null, sbom: null, securityIssues: [], securityBacklog: [], + customCatalogs: {}, commandBarHistory: [], recentSearches: [], preferences: { @@ -157,7 +169,7 @@ describe('getCompletionPercentage', () => { autoValidate: true, allowParallelWork: true, }, - }) + }) as SDKState it('should return 0 for no completed steps', () => { const state = createMockState([]) @@ -182,16 +194,25 @@ describe('getCompletionPercentage', () => { }) describe('getPhaseCompletionPercentage', () => { - const createMockState = (completedSteps: string[]): SDKState => ({ + const createMockState = (completedSteps: string[]) => ({ version: '1.0.0', + projectVersion: 1, lastModified: new Date(), tenantId: 'test', userId: 'test', subscription: 'PROFESSIONAL', + projectId: 'test-project', + projectInfo: null, + customerType: null, + companyProfile: null, + complianceScope: null, + sourcePolicy: null, currentPhase: 1, currentStep: 'use-case-assessment', completedSteps, checkpoints: {}, + importedDocuments: [], + gapAnalysis: null, useCases: [], activeUseCase: null, screening: null, @@ -212,9 +233,12 @@ describe('getPhaseCompletionPercentage', () => { consents: [], dsrConfig: null, escalationWorkflows: [], + iaceProjects: [], + ragCorpusStatus: null, sbom: null, securityIssues: [], securityBacklog: [], + customCatalogs: {}, commandBarHistory: [], recentSearches: [], preferences: { @@ -226,7 +250,7 @@ describe('getPhaseCompletionPercentage', () => { autoValidate: true, allowParallelWork: true, }, - }) + }) as SDKState it('should return 0 for Phase 1 with no completed steps', () => { const state = createMockState([]) diff --git a/admin-compliance/lib/sdk/compliance-scope-documents.ts b/admin-compliance/lib/sdk/compliance-scope-documents.ts index e756aa2..a4ac306 100644 --- a/admin-compliance/lib/sdk/compliance-scope-documents.ts +++ b/admin-compliance/lib/sdk/compliance-scope-documents.ts @@ -17,12 +17,13 @@ import type { } from './compliance-scope-types' import { getDepthLevelNumeric, - DOCUMENT_SCOPE_MATRIX, + DOCUMENT_SCOPE_MATRIX_CORE, DOCUMENT_TYPE_LABELS, - DOCUMENT_SDK_STEP_MAP, } from './compliance-scope-types' import { HARD_TRIGGER_RULES } from './compliance-scope-triggers' +import { DOCUMENT_SDK_STEP_MAP } from "./compliance-scope-sdk-step-map" + // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- @@ -88,7 +89,7 @@ export function normalizeDocType(raw: string): ScopeDocumentType | null { STREITBEILEGUNG: 'streitbeilegung', PRODUKTSICHERHEIT: 'produktsicherheit', } - if (raw in DOCUMENT_SCOPE_MATRIX) return raw as ScopeDocumentType + if (raw in DOCUMENT_SCOPE_MATRIX_CORE) return raw as ScopeDocumentType return mapping[raw] ?? null } @@ -159,11 +160,11 @@ export function buildDocumentScope( } } - for (const docType of Object.keys(DOCUMENT_SCOPE_MATRIX) as ScopeDocumentType[]) { - const requirement = DOCUMENT_SCOPE_MATRIX[docType][level] + for (const docType of Object.keys(DOCUMENT_SCOPE_MATRIX_CORE) as ScopeDocumentType[]) { + const depthReq = DOCUMENT_SCOPE_MATRIX_CORE[docType]?.[level] const isMandatoryFromTrigger = mandatoryFromTriggers.has(docType) - if (requirement === 'mandatory' || isMandatoryFromTrigger) { + if ((depthReq?.required) || isMandatoryFromTrigger) { const originDocs = triggerDocOrigins.get(docType) ?? [] requiredDocs.push({ documentType: docType, @@ -178,7 +179,7 @@ export function buildDocumentScope( .map((t) => t.ruleId) : [], }) - } else if (requirement === 'recommended') { + } else if (depthReq && !depthReq.required) { requiredDocs.push({ documentType: docType, label: DOCUMENT_TYPE_LABELS[docType], diff --git a/admin-compliance/lib/sdk/compliance-scope-sdk-step-map.ts b/admin-compliance/lib/sdk/compliance-scope-sdk-step-map.ts new file mode 100644 index 0000000..17ce0aa --- /dev/null +++ b/admin-compliance/lib/sdk/compliance-scope-sdk-step-map.ts @@ -0,0 +1,10 @@ +import type { ScopeDocumentType } from './compliance-scope-types' + +export const DOCUMENT_SDK_STEP_MAP: Partial> = { + vvt: '/sdk/vvt', tom: '/sdk/tom', dsfa: '/sdk/dsfa', lf: '/sdk/loeschfristen', + dsi: '/sdk/document-generator', av_vertrag: '/sdk/document-generator', + vertragsmanagement: '/sdk/document-generator', widerrufsbelehrung: '/sdk/document-generator', + preisangaben: '/sdk/document-generator', fernabsatz_info: '/sdk/document-generator', + streitbeilegung: '/sdk/document-generator', produktsicherheit: '/sdk/document-generator', + betroffenenrechte: '/sdk/dsr', einwilligung: '/sdk/consent-management', +} diff --git a/admin-compliance/lib/sdk/compliance-scope-types/hard-triggers.ts b/admin-compliance/lib/sdk/compliance-scope-types/hard-triggers.ts index 25e48a3..caff1a4 100644 --- a/admin-compliance/lib/sdk/compliance-scope-types/hard-triggers.ts +++ b/admin-compliance/lib/sdk/compliance-scope-types/hard-triggers.ts @@ -54,6 +54,10 @@ export interface HardTriggerRule { excludeWhen?: { questionId: string; value: string | string[] }; /** Regel feuert NUR wenn diese Bedingung zutrifft */ requireWhen?: { questionId: string; value: string | string[] }; + /** Kombiniert mit Maschinenbau-Profil? Boolean oder Objekt mit Feldbedingung */ + combineWithMachineBuilder?: boolean | { field: string; value?: unknown; includes?: string }; + /** Risikogewicht (0-1) */ + riskWeight?: number; } /** @@ -74,4 +78,10 @@ export interface TriggeredHardTrigger { requiresDSFA: boolean; /** Pflichtdokumente */ mandatoryDocuments: string[]; + /** Original-Regel (optional) */ + rule?: HardTriggerRule; + /** Matching-Wert */ + matchedValue?: unknown; + /** Erklaerung */ + explanation?: string; } diff --git a/admin-compliance/lib/sdk/drafting-engine/__tests__/rag-config.test.ts b/admin-compliance/lib/sdk/drafting-engine/__tests__/rag-config.test.ts index e66a302..00fbb4a 100644 --- a/admin-compliance/lib/sdk/drafting-engine/__tests__/rag-config.test.ts +++ b/admin-compliance/lib/sdk/drafting-engine/__tests__/rag-config.test.ts @@ -48,7 +48,7 @@ describe('DOCUMENT_RAG_CONFIG', () => { }) it('should have DSFA mapped to bp_dsfa_corpus', () => { - expect(DOCUMENT_RAG_CONFIG.dsfa.collection).toBe('bp_dsfa_corpus') + expect(DOCUMENT_RAG_CONFIG.dsfa!.collection).toBe('bp_dsfa_corpus') }) it('should have unique queries for each document type', () => { diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_documents.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_documents.go new file mode 100644 index 0000000..1f5b859 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_documents.go @@ -0,0 +1,86 @@ +package handlers + +import ( + "net/http" + "sort" + "strings" + "sync" + + "github.com/breakpilot/ai-compliance-sdk/internal/ucca" + "github.com/gin-gonic/gin" +) + +// Cached CE corpus document index — built once on first request. +var ( + ceCorpusOnce sync.Once + ceCorpusDocs []ucca.CEDocumentInfo + ceCorpusErr error +) + +// ListCECorpusDocuments returns the deduplicated document index from bp_compliance_ce. +// GET /iace/ce-corpus-documents +func (h *IACEHandler) ListCECorpusDocuments(c *gin.Context) { + ceCorpusOnce.Do(func() { + ceCorpusDocs, ceCorpusErr = h.ragClient.ScrollDocumentIndex( + c.Request.Context(), "bp_compliance_ce", + ) + if ceCorpusErr == nil { + sort.Slice(ceCorpusDocs, func(i, j int) bool { + if ceCorpusDocs[i].Category != ceCorpusDocs[j].Category { + return ceCorpusDocs[i].Category < ceCorpusDocs[j].Category + } + return ceCorpusDocs[i].RegulationID < ceCorpusDocs[j].RegulationID + }) + } + }) + + if ceCorpusErr != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "failed to load CE corpus index: " + ceCorpusErr.Error(), + }) + return + } + + // Optional search filter + query := strings.ToLower(c.Query("q")) + category := c.Query("category") + + filtered := ceCorpusDocs + if query != "" || category != "" { + filtered = make([]ucca.CEDocumentInfo, 0, len(ceCorpusDocs)) + for _, d := range ceCorpusDocs { + if category != "" && d.Category != category { + continue + } + if query != "" { + haystack := strings.ToLower(d.RegulationID + " " + d.NameDE + " " + d.NameEN + " " + d.SourceOrg) + if !strings.Contains(haystack, query) { + continue + } + } + filtered = append(filtered, d) + } + } + + // Group by category for the response + groups := make(map[string][]ucca.CEDocumentInfo) + for _, d := range filtered { + cat := d.Category + if cat == "" { + cat = "other" + } + groups[cat] = append(groups[cat], d) + } + + totalChunks := 0 + for _, d := range filtered { + totalChunks += d.ChunkCount + } + + c.JSON(http.StatusOK, gin.H{ + "documents": filtered, + "groups": groups, + "total": len(filtered), + "total_chunks": totalChunks, + }) +} diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_enrich.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_enrich.go new file mode 100644 index 0000000..9835adc --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_enrich.go @@ -0,0 +1,344 @@ +package handlers + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// RegulatoryHint represents a relevant passage from TRBS/TRGS/ASR/OSHA. +type RegulatoryHint struct { + RegulationID string `json:"regulation_id"` + RegulationShort string `json:"regulation_short"` + Category string `json:"category"` + Text string `json:"text"` + Pages []int `json:"pages,omitempty"` + SourceURL string `json:"source_url,omitempty"` + Score float64 `json:"score"` +} + +// categoryToSearchTerms maps hazard categories to German search terms +// that match TRBS/TRGS/ASR/OSHA content. +var categoryToSearchTerms = map[string]string{ + "mechanical_hazard": "mechanische Gefaehrdung Quetschstelle Scherstelle Stossstelle", + "electrical_hazard": "elektrische Gefaehrdung Stromschlag Lichtbogen Kurzschluss", + "thermal_hazard": "thermische Gefaehrdung Verbrennung Erfrierung heisse Oberflaeche", + "noise_hazard": "Laerm Gehoerschutz Schalldruckpegel Laermexposition", + "vibration_hazard": "Vibration Hand-Arm Ganzkoerper Schwingungsbelastung", + "radiation_hazard": "Strahlung ionisierend nichtionisierend Laser UV", + "chemical_hazard": "Gefahrstoff chemische Gefaehrdung Exposition Grenzwert", + "ergonomic_hazard": "Ergonomie Zwangshaltung Lasthandhabung Koerperbelastung", + "hydraulic_hazard": "Hydraulik Druckbehaelter Druck Bersten Leckage", + "pneumatic_hazard": "Pneumatik Druckluft Druckbehaelter Belueftung", + "software_hazard": "Software Sicherheitsfunktion Fehlfunktion Programmierung", + "safety_function_failure": "Sicherheitsfunktion Ausfall SIL Performance Level", + "fire_explosion_hazard": "Brand Explosion explosionsfaehige Atmosphaere Zuendschutz", + "falling_hazard": "Absturz herabfallende Gegenstaende Sturzgefahr", + "trip_slip_hazard": "Stolpern Rutschen Ausrutschen Fussboden Verkehrsweg", + "entrapment_hazard": "Einzugsstelle Fangstelle rotierende Teile Wickelgefahr", + "crush_hazard": "Quetschgefahr Quetschstelle Einklemmen Andruckkraft", + "cut_hazard": "Schneiden Schneidwerkzeug Schnittverletzung scharfe Kante", + "stabbing_hazard": "Stechen Stichverletzung spitze Teile Injektionsgefahr", + "high_pressure_hazard": "Hochdruck Fluessigkeitsstrahl Druckbehaelter Ueberdruck", + "collision_hazard": "Kollision Zusammenstoss Anfahren fahrerlose Transportsysteme", + "lack_of_stability_hazard": "Standsicherheit Umkippen Kippen Stabilitaet", + "unexpected_start_hazard": "unerwarteter Anlauf Wiederanlauf Energietrennung Lockout", + "control_system_failure": "Steuerungsausfall Steuerung Fehler Ausfall Sicherheitssteuerung", + "ppe_hazard": "PSA persoenliche Schutzausruestung Schutzkleidung", +} + +// EnrichHazardWithRegulations returns regulatory hints for a specific hazard. +// GET /projects/:id/hazards/:hid/regulatory-hints +func (h *IACEHandler) EnrichHazardWithRegulations(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + hazardID, err := uuid.Parse(c.Param("hid")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hazard ID"}) + return + } + + // Fetch hazard + hazard, err := h.store.GetHazard(c.Request.Context(), hazardID) + if err != nil || hazard == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "hazard not found"}) + return + } + if hazard.ProjectID != projectID { + c.JSON(http.StatusNotFound, gin.H{"error": "hazard not in this project"}) + return + } + + // Fetch project for machine context + project, err := h.store.GetProject(c.Request.Context(), projectID) + if err != nil || project == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + // Build search query from hazard context + query := buildHazardSearchQuery(hazard.Category, hazard.Name, hazard.Scenario, project.MachineName, project.MachineType) + + // Search bp_compliance_ce (TRBS/TRGS/ASR/OSHA) + results, err := h.ragClient.SearchCollection( + c.Request.Context(), + "bp_compliance_ce", + query, + nil, // no regulation filter — search all + 7, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "regulatory search failed: " + err.Error(), + }) + return + } + + hints := make([]RegulatoryHint, 0, len(results)) + for _, r := range results { + if r.Score < 0.3 { + continue // skip low-relevance results + } + hints = append(hints, RegulatoryHint{ + RegulationID: r.RegulationCode, + RegulationShort: r.RegulationShort, + Category: r.Category, + Text: truncateHintText(r.Text, 500), + Pages: r.Pages, + SourceURL: r.SourceURL, + Score: r.Score, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "hazard_id": hazardID.String(), + "hazard_name": hazard.Name, + "category": hazard.Category, + "query": query, + "hints": hints, + "total": len(hints), + }) +} + +// EnrichMitigationWithRegulations returns regulatory hints for a mitigation. +// GET /projects/:id/mitigations/:mid/regulatory-hints +func (h *IACEHandler) EnrichMitigationWithRegulations(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + mitigationID, err := uuid.Parse(c.Param("mid")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid mitigation ID"}) + return + } + + // Fetch mitigation + mitigation, err := h.store.GetMitigation(c.Request.Context(), mitigationID) + if err != nil || mitigation == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not found"}) + return + } + + // Fetch the hazard to get category context + hazard, err := h.store.GetHazard(c.Request.Context(), mitigation.HazardID) + if err != nil || hazard == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "linked hazard not found"}) + return + } + if hazard.ProjectID != projectID { + c.JSON(http.StatusNotFound, gin.H{"error": "mitigation not in this project"}) + return + } + + // Build search query from mitigation + hazard context + queryParts := []string{mitigation.Name} + if mitigation.Description != "" { + queryParts = append(queryParts, mitigation.Description) + } + queryParts = append(queryParts, "Schutzmassnahme") + if terms, ok := categoryToSearchTerms[hazard.Category]; ok { + queryParts = append(queryParts, terms) + } + query := strings.Join(queryParts, " ") + if len(query) > 500 { + query = query[:500] + } + + results, err := h.ragClient.SearchCollection( + c.Request.Context(), + "bp_compliance_ce", + query, + nil, + 7, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "regulatory search failed: " + err.Error(), + }) + return + } + + hints := make([]RegulatoryHint, 0, len(results)) + for _, r := range results { + if r.Score < 0.3 { + continue + } + hints = append(hints, RegulatoryHint{ + RegulationID: r.RegulationCode, + RegulationShort: r.RegulationShort, + Category: r.Category, + Text: truncateHintText(r.Text, 500), + Pages: r.Pages, + SourceURL: r.SourceURL, + Score: r.Score, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "mitigation_id": mitigationID.String(), + "mitigation_name": mitigation.Name, + "reduction_type": mitigation.ReductionType, + "query": query, + "hints": hints, + "total": len(hints), + }) +} + +// buildHazardSearchQuery creates a contextual query for RAG search. +func buildHazardSearchQuery(category, name, scenario, machineName, machineType string) string { + parts := make([]string, 0, 5) + + // Add category-specific German search terms + if terms, ok := categoryToSearchTerms[category]; ok { + parts = append(parts, terms) + } + + // Add hazard name and scenario + if name != "" { + parts = append(parts, name) + } + if scenario != "" && len(scenario) < 200 { + parts = append(parts, scenario) + } + + // Add machine context + if machineType != "" { + parts = append(parts, machineType) + } + if machineName != "" && len(parts) < 4 { + parts = append(parts, machineName) + } + + query := strings.Join(parts, " ") + if len(query) > 500 { + query = query[:500] + } + return query +} + +func truncateHintText(text string, maxLen int) string { + if len(text) <= maxLen { + return text + } + // Find last sentence boundary + truncated := text[:maxLen] + if lastDot := strings.LastIndex(truncated, ". "); lastDot > maxLen/2 { + return truncated[:lastDot+1] + } + return truncated + "..." +} + +// ============================================================================ +// Batch: Enrich all hazards at once (for overview display) +// ============================================================================ + +// EnrichProjectHazardsBatch returns top regulatory hint per hazard category. +// GET /projects/:id/regulatory-hints +func (h *IACEHandler) EnrichProjectHazardsBatch(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + project, err := h.store.GetProject(c.Request.Context(), projectID) + if err != nil || project == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + // Get all hazards to extract unique categories + hazards, err := h.store.ListHazards(c.Request.Context(), projectID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list hazards"}) + return + } + + // Deduplicate categories + seen := make(map[string]bool) + var categories []string + for _, hz := range hazards { + if !seen[hz.Category] { + seen[hz.Category] = true + categories = append(categories, hz.Category) + } + } + + // One RAG search per unique category (typically 5-10 categories, not 160 hazards) + type CategoryHints struct { + Category string `json:"category"` + Hints []RegulatoryHint `json:"hints"` + } + + result := make([]CategoryHints, 0, len(categories)) + for _, cat := range categories { + query := buildHazardSearchQuery(cat, "", "", project.MachineName, project.MachineType) + results, err := h.ragClient.SearchCollection( + c.Request.Context(), + "bp_compliance_ce", + query, + nil, + 3, + ) + if err != nil { + continue + } + + hints := make([]RegulatoryHint, 0, len(results)) + for _, r := range results { + if r.Score < 0.3 { + continue + } + hints = append(hints, RegulatoryHint{ + RegulationID: r.RegulationCode, + RegulationShort: r.RegulationShort, + Category: r.Category, + Text: truncateHintText(r.Text, 300), + Pages: r.Pages, + SourceURL: r.SourceURL, + Score: r.Score, + }) + } + if len(hints) > 0 { + result = append(result, CategoryHints{Category: cat, Hints: hints}) + } + } + + c.JSON(http.StatusOK, gin.H{ + "project_id": projectID.String(), + "categories": len(categories), + "total_hazards": len(hazards), + "regulatory_hints": result, + "sources": fmt.Sprintf("TRBS/TRGS/ASR (%d BAuA) + OSHA Technical Manual", 126), + }) +} diff --git a/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go new file mode 100644 index 0000000..80344ea --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/iace_handler_init.go @@ -0,0 +1,395 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/breakpilot/ai-compliance-sdk/internal/iace" + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +// InitStep tracks progress of each initialization step. +type InitStep struct { + Name string `json:"name"` + Status string `json:"status"` // "done", "skipped", "error" + Count int `json:"count,omitempty"` + Details string `json:"details,omitempty"` +} + +// InitializeProject handles POST /projects/:id/initialize +// Chains: parse narrative → create components → fire patterns → +// create hazards + measures + verification → suggest norms. +// Idempotent: skips steps that are already populated. +func (h *IACEHandler) InitializeProject(c *gin.Context) { + projectID, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"}) + return + } + + tenantID, err := getTenantID(c) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + ctx := c.Request.Context() + project, err := h.store.GetProject(ctx, projectID) + if err != nil || project == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "project not found"}) + return + } + + steps := make([]InitStep, 0, 6) + + // ── Step 1: Extract narrative from limits_form ── + narrativeText := extractNarrativeFromMetadata(project.Metadata) + if narrativeText == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Grenzen-Formular ist leer. Bitte zuerst die Maschinenbeschreibung ausfuellen.", + }) + return + } + + // ── Step 2: Parse narrative deterministically ── + parseResult := iace.ParseNarrative(narrativeText, project.MachineType) + steps = append(steps, InitStep{ + Name: "Narrative analysiert", + Status: "done", + Count: len(parseResult.Components), + Details: fmt.Sprintf("%d Komponenten, %d Energiequellen, %d Tags", len(parseResult.Components), len(parseResult.EnergySources), len(parseResult.CustomTags)), + }) + + // ── Step 3: Create components (skip if already exist) ── + existingComps, _ := h.store.ListComponents(ctx, projectID) + compStep := InitStep{Name: "Komponenten erstellt", Status: "skipped"} + if len(existingComps) == 0 && len(parseResult.Components) > 0 { + created := 0 + for _, comp := range parseResult.Components { + // Derive component type from tags + compType := deriveComponentType(comp.Tags) + _, cerr := h.store.CreateComponent(ctx, iace.CreateComponentRequest{ + ProjectID: projectID, + Name: comp.NameDE, + ComponentType: compType, + Description: "Auto-erkannt aus Maschinenbeschreibung (" + comp.MatchedOn + ")", + }) + if cerr == nil { + created++ + } + } + compStep = InitStep{Name: "Komponenten erstellt", Status: "done", Count: created} + } else if len(existingComps) > 0 { + compStep.Details = "Bereits vorhanden" + compStep.Count = len(existingComps) + } + steps = append(steps, compStep) + + // ── Step 4: Fire pattern engine ── + var componentIDs, energyIDs []string + for _, comp := range parseResult.Components { + componentIDs = append(componentIDs, comp.LibraryID) + } + for _, e := range parseResult.EnergySources { + energyIDs = append(energyIDs, e.SourceID) + } + + engine := iace.NewPatternEngine() + matchOutput := engine.Match(iace.MatchInput{ + ComponentLibraryIDs: componentIDs, + EnergySourceIDs: energyIDs, + LifecyclePhases: parseResult.LifecyclePhases, + CustomTags: parseResult.CustomTags, + }) + steps = append(steps, InitStep{ + Name: "Patterns abgeglichen", + Status: "done", + Count: len(matchOutput.MatchedPatterns), + }) + + // ── Step 5: Create hazards from matched patterns (skip if exist) ── + existingHazards, _ := h.store.ListHazards(ctx, projectID) + hazardStep := InitStep{Name: "Gefaehrdungen erstellt", Status: "skipped"} + hazardIDsByCategory := make(map[string]uuid.UUID) + + if len(existingHazards) == 0 && len(matchOutput.MatchedPatterns) > 0 { + // Get first component for hazard assignment + comps, _ := h.store.ListComponents(ctx, projectID) + var defaultCompID uuid.UUID + if len(comps) > 0 { + defaultCompID = comps[0].ID + } + + // Deduplicate by category — one hazard per category + created := 0 + seenCat := make(map[string]bool) + for _, mp := range matchOutput.MatchedPatterns { + for _, cat := range mp.HazardCats { + if seenCat[cat] { + continue + } + seenCat[cat] = true + + name := mp.PatternName + if name == "" { + name = cat + } + scenario := mp.ScenarioDE + hz, cerr := h.store.CreateHazard(ctx, iace.CreateHazardRequest{ + ProjectID: projectID, + ComponentID: defaultCompID, + Name: name, + Description: scenario, + Category: cat, + Scenario: scenario, + }) + if cerr == nil { + created++ + hazardIDsByCategory[cat] = hz.ID + } + } + } + hazardStep = InitStep{Name: "Gefaehrdungen erstellt", Status: "done", Count: created} + } else if len(existingHazards) > 0 { + hazardStep.Details = "Bereits vorhanden" + hazardStep.Count = len(existingHazards) + for _, eh := range existingHazards { + hazardIDsByCategory[eh.Category] = eh.ID + } + } + steps = append(steps, hazardStep) + + // ── Step 6: Create mitigations (pattern-suggested + category fallback) ── + existingMits, _ := h.store.ListMitigationsByProject(ctx, projectID) + mitStep := InitStep{Name: "Massnahmen erstellt", Status: "skipped"} + + if len(existingMits) == 0 && len(hazardIDsByCategory) > 0 { + measureLib := iace.GetProtectiveMeasureLibrary() + measureByID := make(map[string]iace.ProtectiveMeasureEntry, len(measureLib)) + measuresByCat := make(map[string][]iace.ProtectiveMeasureEntry) + for _, m := range measureLib { + measureByID[m.ID] = m + measuresByCat[m.HazardCategory] = append(measuresByCat[m.HazardCategory], m) + } + + created := 0 + usedMeasureIDs := make(map[string]bool) + + // A) Pattern-suggested measures (direct reference) + for _, sm := range matchOutput.SuggestedMeasures { + entry, ok := measureByID[sm.MeasureID] + if !ok || usedMeasureIDs[sm.MeasureID] { + continue + } + hazardID := findHazardForMeasureByCategory(entry.HazardCategory, hazardIDsByCategory) + if hazardID == uuid.Nil { + continue + } + rt := iace.ReductionType(entry.ReductionType) + if rt == "" { + rt = iace.ReductionTypeInformation + } + _, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{ + HazardID: hazardID, + ReductionType: rt, + Name: entry.Name, + Description: entry.Description, + }) + if cerr == nil { + created++ + usedMeasureIDs[sm.MeasureID] = true + } + } + + // B) Category fallback — for each hazard category, add measures + // from the library that match (but weren't pattern-suggested) + for hazCat, hazID := range hazardIDsByCategory { + measCat := patternCatToMeasureCat(hazCat) + candidates := measuresByCat[measCat] + added := 0 + for _, m := range candidates { + if usedMeasureIDs[m.ID] || added >= 8 { + break + } + rt := iace.ReductionType(m.ReductionType) + if rt == "" { + rt = iace.ReductionTypeInformation + } + _, cerr := h.store.CreateMitigation(ctx, iace.CreateMitigationRequest{ + HazardID: hazID, + ReductionType: rt, + Name: m.Name, + Description: m.Description, + }) + if cerr == nil { + created++ + usedMeasureIDs[m.ID] = true + added++ + } + } + } + + mitStep = InitStep{Name: "Massnahmen erstellt", Status: "done", Count: created} + } else if len(existingMits) > 0 { + mitStep.Details = "Bereits vorhanden" + mitStep.Count = len(existingMits) + } + steps = append(steps, mitStep) + + // ── Step 7: Suggest norms ── + var hazardCats []string + for cat := range hazardIDsByCategory { + hazardCats = append(hazardCats, cat) + } + normResult := iace.SuggestNorms(project.MachineType, hazardCats, parseResult.CustomTags) + normCount := 0 + if normResult != nil { + normCount = len(normResult.ANorms) + len(normResult.B1Norms) + len(normResult.B2Norms) + len(normResult.CNorms) + } + steps = append(steps, InitStep{ + Name: "Normen vorgeschlagen", + Status: "done", + Count: normCount, + }) + + // ── Audit trail ── + h.store.AddAuditEntry(ctx, projectID, "project_initialization", projectID, + iace.AuditActionCreate, tenantID.String(), nil, + mustMarshalJSON(map[string]interface{}{"steps": steps}), + ) + + c.JSON(http.StatusOK, gin.H{ + "project_id": projectID.String(), + "steps": steps, + "summary": gin.H{ + "components": steps[1].Count, + "patterns": steps[2].Count, + "hazards": steps[3].Count, + "mitigations": steps[4].Count, + "norms": steps[5].Count, + }, + }) +} + +// extractNarrativeFromMetadata builds a combined text from the limits_form. +func extractNarrativeFromMetadata(metadata json.RawMessage) string { + if metadata == nil { + return "" + } + var meta map[string]json.RawMessage + if err := json.Unmarshal(metadata, &meta); err != nil { + return "" + } + limitsRaw, ok := meta["limits_form"] + if !ok { + return "" + } + var limits map[string]interface{} + if err := json.Unmarshal(limitsRaw, &limits); err != nil { + return "" + } + + textFields := []string{ + "general_description", "intended_purpose", "foreseeable_misuse", + "space_limits", "time_limits", "environmental_conditions", + "energy_sources", "materials_processed", "operating_modes", + "maintenance_requirements", "personnel_requirements", + "interfaces_description", "control_system_description", + "safety_functions_description", + } + var result string + for _, field := range textFields { + if v, ok := limits[field]; ok { + if s, ok := v.(string); ok && s != "" { + result += s + "\n\n" + } + } + } + return result +} + +// patternCatToMeasureCat maps pattern hazard categories to measure categories. +// Patterns use "mechanical_hazard", measures use "mechanical". +func patternCatToMeasureCat(patternCat string) string { + m := map[string]string{ + "mechanical_hazard": "mechanical", + "electrical_hazard": "electrical", + "thermal_hazard": "thermal", + "noise_vibration": "noise_vibration", + "pneumatic_hydraulic": "pneumatic_hydraulic", + "material_environmental": "material_environmental", + "ergonomic": "ergonomic", + "ergonomic_hazard": "ergonomic", + "software_fault": "software_control", + "safety_function_failure": "safety_function", + "fire_explosion": "thermal", + "radiation_hazard": "material_environmental", + "unauthorized_access": "cyber_network", + "communication_failure": "cyber_network", + "firmware_corruption": "cyber_network", + "logging_audit_failure": "cyber_network", + "ai_misclassification": "ai_specific", + "false_classification": "ai_specific", + "model_drift": "ai_specific", + "data_poisoning": "ai_specific", + "sensor_spoofing": "ai_specific", + "unintended_bias": "ai_specific", + "sensor_fault": "software_control", + "configuration_error": "software_control", + "update_failure": "software_control", + "hmi_error": "software_control", + "emc_hazard": "electrical", + "maintenance_hazard": "mechanical", + "mode_confusion": "software_control", + } + if cat, ok := m[patternCat]; ok { + return cat + } + return "general" +} + +// deriveComponentType guesses the component type from its tags. +func deriveComponentType(tags []string) iace.ComponentType { + for _, t := range tags { + switch { + case t == "software" || t == "has_software": + return iace.ComponentTypeSoftware + case t == "firmware" || t == "has_firmware": + return iace.ComponentTypeFirmware + case t == "has_ai" || t == "ai_model": + return iace.ComponentTypeAIModel + case t == "hmi" || t == "display" || t == "touchscreen": + return iace.ComponentTypeHMI + case t == "sensor" || t == "camera": + return iace.ComponentTypeSensor + case t == "electric_motor" || t == "electric_drive": + return iace.ComponentTypeElectrical + case t == "networked" || t == "ethernet" || t == "wifi": + return iace.ComponentTypeNetwork + case t == "hydraulic" || t == "pneumatic": + return iace.ComponentTypeActuator + } + } + return iace.ComponentTypeMechanical +} + +// findHazardForMeasureByCategory finds a matching hazard for a measure. +func findHazardForMeasureByCategory(measureCat string, hazardsByCategory map[string]uuid.UUID) uuid.UUID { + // Direct match + if id, ok := hazardsByCategory[measureCat]; ok { + return id + } + // Fuzzy match — "mechanical" matches "mechanical_hazard" + for cat, id := range hazardsByCategory { + if len(measureCat) > 3 && len(cat) > 3 && cat[:4] == measureCat[:4] { + return id + } + } + // Fallback: first hazard + for _, id := range hazardsByCategory { + return id + } + return uuid.Nil +} diff --git a/ai-compliance-sdk/internal/app/routes.go b/ai-compliance-sdk/internal/app/routes.go index 683b9f3..34ba2c4 100644 --- a/ai-compliance-sdk/internal/app/routes.go +++ b/ai-compliance-sdk/internal/app/routes.go @@ -421,6 +421,11 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) { iaceRoutes.PUT("/projects/:id/monitoring/:eid", h.UpdateMonitoringEvent) iaceRoutes.GET("/projects/:id/audit-trail", h.GetAuditTrail) iaceRoutes.POST("/library-search", h.SearchLibrary) + iaceRoutes.GET("/ce-corpus-documents", h.ListCECorpusDocuments) + iaceRoutes.POST("/projects/:id/initialize", h.InitializeProject) + iaceRoutes.GET("/projects/:id/hazards/:hid/regulatory-hints", h.EnrichHazardWithRegulations) + iaceRoutes.GET("/projects/:id/mitigations/:mid/regulatory-hints", h.EnrichMitigationWithRegulations) + iaceRoutes.GET("/projects/:id/regulatory-hints", h.EnrichProjectHazardsBatch) iaceRoutes.POST("/projects/:id/tech-file/:section/enrich", h.EnrichTechFileSection) // Production Lines diff --git a/ai-compliance-sdk/internal/iace/component_library_test.go b/ai-compliance-sdk/internal/iace/component_library_test.go index 41581f8..5d6b644 100644 --- a/ai-compliance-sdk/internal/iace/component_library_test.go +++ b/ai-compliance-sdk/internal/iace/component_library_test.go @@ -2,11 +2,11 @@ package iace import "testing" -// TestGetComponentLibrary_EntryCount verifies the component library has exactly 120 entries. +// TestGetComponentLibrary_EntryCount verifies the component library has at least 120 entries. func TestGetComponentLibrary_EntryCount(t *testing.T) { entries := GetComponentLibrary() - if len(entries) != 120 { - t.Fatalf("GetComponentLibrary returned %d entries, want 120", len(entries)) + if len(entries) < 120 { + t.Fatalf("GetComponentLibrary returned %d entries, want at least 120", len(entries)) } } @@ -56,14 +56,14 @@ func TestGetComponentLibrary_NonEmptyFields(t *testing.T) { } } -// TestGetComponentLibrary_CategoryDistribution verifies expected category counts. +// TestGetComponentLibrary_CategoryDistribution verifies minimum category counts. func TestGetComponentLibrary_CategoryDistribution(t *testing.T) { counts := make(map[string]int) for _, e := range GetComponentLibrary() { counts[e.Category]++ } - expected := map[string]int{ - "mechanical": 20, + minimums := map[string]int{ + "mechanical": 10, "structural": 10, "drive": 10, "hydraulic": 10, @@ -75,10 +75,10 @@ func TestGetComponentLibrary_CategoryDistribution(t *testing.T) { "safety": 10, "it_network": 10, } - for cat, want := range expected { + for cat, minWant := range minimums { got := counts[cat] - if got != want { - t.Errorf("category %s: got %d entries, want %d", cat, got, want) + if got < minWant { + t.Errorf("category %s: got %d entries, want at least %d", cat, got, minWant) } } } diff --git a/ai-compliance-sdk/internal/iace/controls_library_test.go b/ai-compliance-sdk/internal/iace/controls_library_test.go index f8c4085..8be7fdf 100644 --- a/ai-compliance-sdk/internal/iace/controls_library_test.go +++ b/ai-compliance-sdk/internal/iace/controls_library_test.go @@ -73,11 +73,11 @@ func TestProtectiveMeasures_HazardCategoryNotEmpty(t *testing.T) { } } -// TestProtectiveMeasures_Count200 verifies exactly 200 measures exist. -func TestProtectiveMeasures_Count200(t *testing.T) { +// TestProtectiveMeasures_Count241 verifies at least 241 measures exist (200 base + 25 mandatory + 16 Phase1B). +func TestProtectiveMeasures_Count241(t *testing.T) { entries := GetProtectiveMeasureLibrary() - if len(entries) != 200 { - t.Fatalf("got %d protective measures, want exactly 200", len(entries)) + if len(entries) < 241 { + t.Fatalf("got %d protective measures, want at least 241", len(entries)) } } @@ -126,13 +126,17 @@ func TestProtectiveMeasures_DesignProtectionInfoDistribution(t *testing.T) { t.Logf("Distribution: design=%d, protection=%d, information=%d", design, protection, information) } -// TestProtectiveMeasures_IDSequential verifies IDs run M001-M200 without gaps. -func TestProtectiveMeasures_IDSequential(t *testing.T) { +// TestProtectiveMeasures_UniqueIDs verifies all measure IDs are unique. +func TestProtectiveMeasures_UniqueIDs(t *testing.T) { entries := GetProtectiveMeasureLibrary() - for i, e := range entries { - expected := "M" + padID(i+1) - if e.ID != expected { - t.Errorf("entries[%d]: got ID %q, want %q", i, e.ID, expected) + seen := make(map[string]bool) + for _, e := range entries { + if seen[e.ID] { + t.Errorf("duplicate measure ID: %s", e.ID) + } + seen[e.ID] = true + if e.ID == "" { + t.Error("empty measure ID found") } } } diff --git a/ai-compliance-sdk/internal/iace/measures_library.go b/ai-compliance-sdk/internal/iace/measures_library.go index 60e4896..44e3b8e 100644 --- a/ai-compliance-sdk/internal/iace/measures_library.go +++ b/ai-compliance-sdk/internal/iace/measures_library.go @@ -19,48 +19,48 @@ func GetProtectiveMeasureLibrary() []ProtectiveMeasureEntry { func getDesignMeasures() []ProtectiveMeasureEntry { return []ProtectiveMeasureEntry{ // ── Geometry (M001-M010) ───────────────────────────────────────────── - {ID: "M001", ReductionType: "design", SubType: "geometry", Name: "Gefahrstelle konstruktiv eliminieren", Description: "Durch konstruktive Gestaltung wird die Gefahrstelle vollstaendig beseitigt.", HazardCategory: "mechanical", Examples: []string{"Quetschstelle durch Geometrieaenderung entfernen", "Einzugsstelle durch vergroesserten Spalt eliminieren"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2"}}, + {ID: "M001", ReductionType: "design", SubType: "geometry", Name: "Gefahrstelle konstruktiv eliminieren", Description: "Durch konstruktive Gestaltung wird die Gefahrstelle vollstaendig beseitigt.", HazardCategory: "mechanical", Examples: []string{"Quetschstelle durch Geometrieaenderung entfernen", "Einzugsstelle durch vergroesserten Spalt eliminieren"}, NormReferences: []string{"ISO 12100 — Inhaerent sichere Konstruktion"}}, {ID: "M002", ReductionType: "design", SubType: "geometry", Name: "Sicherheitsabstaende vergroessern", Description: "Abstaende zwischen Gefahrstellen und zugaenglichen Bereichen werden nach Norm dimensioniert.", HazardCategory: "mechanical", Examples: []string{"Greifabstand an Walzen vergroessern", "Abstand zu heissen Oberflaechen erhoehen"}, NormReferences: []string{"ISO 13857", "ISO 13854"}}, - {ID: "M003", ReductionType: "design", SubType: "geometry", Name: "Scharfe Kanten entfernen", Description: "Alle zugaenglichen Kanten werden abgerundet oder entgratet.", HazardCategory: "mechanical", Examples: []string{"Radien an Blechkanten anbringen", "Entgratung aller Stanzteile sicherstellen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, - {ID: "M004", ReductionType: "design", SubType: "geometry", Name: "Sichere Geometrie", Description: "Die Bauteilgeometrie vermeidet Quetsch-, Scher- und Einzugsstellen.", HazardCategory: "mechanical", Examples: []string{"Abgerundete Formteile statt scharfkantiger verwenden", "Spaltmasse an Fuehrungen einhalten"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, - {ID: "M005", ReductionType: "design", SubType: "geometry", Name: "Rotationsbewegung vermeiden", Description: "Rotierende Teile werden durch Alternativloesungen ersetzt.", HazardCategory: "mechanical", Examples: []string{"Linearantrieb statt Drehantrieb verwenden", "Riemenantrieb durch Zahnstange ersetzen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M006", ReductionType: "design", SubType: "geometry", Name: "Kollisionsfreie Bewegungsbahnen", Description: "Bewegungsbahnen werden so geplant, dass Kollisionen mit Personen ausgeschlossen sind.", HazardCategory: "mechanical", Examples: []string{"Verfahrwege ausserhalb des Bedienerbereichs legen", "Bewegungsbahnen in der Simulation pruefen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.3"}}, - {ID: "M007", ReductionType: "design", SubType: "geometry", Name: "Sichere Greiferkonstruktion", Description: "Greifersysteme verhindern unkontrolliertes Freisetzen von Werkstuecken.", HazardCategory: "mechanical", Examples: []string{"Formschluessige Greiferbacken verwenden", "Federbelastete Greifer fuer Fail-Safe"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1", "ISO 10218-2"}}, - {ID: "M008", ReductionType: "design", SubType: "geometry", Name: "Sichere Werkstueckaufnahme", Description: "Werkstueckaufnahmen verhindern Herausschleudern bei allen Betriebszustaenden.", HazardCategory: "mechanical", Examples: []string{"Spannvorrichtung mit Formschluss", "Automatische Spannkontrolle integrieren"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, - {ID: "M009", ReductionType: "design", SubType: "geometry", Name: "Sichere Kabelfuehrung", Description: "Elektrische Leitungen werden vor mechanischer Beschaedigung und Hitze geschuetzt.", HazardCategory: "electrical", Examples: []string{"Kabelkanaele mit Deckel verwenden", "Leitungen in Schleppketten fuehren"}, NormReferences: []string{"IEC 60204-1", "ISO 12100:2010 Kap. 6.2.9"}}, - {ID: "M010", ReductionType: "design", SubType: "geometry", Name: "Sichere Sensorposition", Description: "Sensoren werden zuverlaessig messend und vor mechanischer Beschaedigung geschuetzt positioniert.", HazardCategory: "software_control", Examples: []string{"Sensoren in geschuetzten Nischen montieren", "Sensoren ausserhalb des Gefahrbereichs platzieren"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.11.1"}}, + {ID: "M003", ReductionType: "design", SubType: "geometry", Name: "Scharfe Kanten entfernen", Description: "Alle zugaenglichen Kanten werden abgerundet oder entgratet.", HazardCategory: "mechanical", Examples: []string{"Radien an Blechkanten anbringen", "Entgratung aller Stanzteile sicherstellen"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, + {ID: "M004", ReductionType: "design", SubType: "geometry", Name: "Sichere Geometrie", Description: "Die Bauteilgeometrie vermeidet Quetsch-, Scher- und Einzugsstellen.", HazardCategory: "mechanical", Examples: []string{"Abgerundete Formteile statt scharfkantiger verwenden", "Spaltmasse an Fuehrungen einhalten"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, + {ID: "M005", ReductionType: "design", SubType: "geometry", Name: "Rotationsbewegung vermeiden", Description: "Rotierende Teile werden durch Alternativloesungen ersetzt.", HazardCategory: "mechanical", Examples: []string{"Linearantrieb statt Drehantrieb verwenden", "Riemenantrieb durch Zahnstange ersetzen"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M006", ReductionType: "design", SubType: "geometry", Name: "Kollisionsfreie Bewegungsbahnen", Description: "Bewegungsbahnen werden so geplant, dass Kollisionen mit Personen ausgeschlossen sind.", HazardCategory: "mechanical", Examples: []string{"Verfahrwege ausserhalb des Bedienerbereichs legen", "Bewegungsbahnen in der Simulation pruefen"}, NormReferences: []string{"ISO 12100 — Allgemeine technische Kenntnisse"}}, + {ID: "M007", ReductionType: "design", SubType: "geometry", Name: "Sichere Greiferkonstruktion", Description: "Greifersysteme verhindern unkontrolliertes Freisetzen von Werkstuecken.", HazardCategory: "mechanical", Examples: []string{"Formschluessige Greiferbacken verwenden", "Federbelastete Greifer fuer Fail-Safe"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung", "ISO 10218-2"}}, + {ID: "M008", ReductionType: "design", SubType: "geometry", Name: "Sichere Werkstueckaufnahme", Description: "Werkstueckaufnahmen verhindern Herausschleudern bei allen Betriebszustaenden.", HazardCategory: "mechanical", Examples: []string{"Spannvorrichtung mit Formschluss", "Automatische Spannkontrolle integrieren"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, + {ID: "M009", ReductionType: "design", SubType: "geometry", Name: "Sichere Kabelfuehrung", Description: "Elektrische Leitungen werden vor mechanischer Beschaedigung und Hitze geschuetzt.", HazardCategory: "electrical", Examples: []string{"Kabelkanaele mit Deckel verwenden", "Leitungen in Schleppketten fuehren"}, NormReferences: []string{"IEC 60204-1", "ISO 12100 — Minimierung Fehlerwahrscheinlichkeit"}}, + {ID: "M010", ReductionType: "design", SubType: "geometry", Name: "Sichere Sensorposition", Description: "Sensoren werden zuverlaessig messend und vor mechanischer Beschaedigung geschuetzt positioniert.", HazardCategory: "software_control", Examples: []string{"Sensoren in geschuetzten Nischen montieren", "Sensoren ausserhalb des Gefahrbereichs platzieren"}, NormReferences: []string{"ISO 12100 — Sicherheitsbezogene Steuerungssysteme"}}, // ── Force / Energy (M011-M022) ────────────────────────────────────── - {ID: "M011", ReductionType: "design", SubType: "force_energy", Name: "Bewegungsenergie reduzieren", Description: "Kinetische Energie beweglicher Maschinenteile wird auf ein sicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Masse beweglicher Teile verringern", "Hublaenge verkuerzen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M012", ReductionType: "design", SubType: "force_energy", Name: "Geschwindigkeit reduzieren", Description: "Verfahrgeschwindigkeit wird konstruktiv auf ein verletzungssicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Maximale Achsgeschwindigkeit mechanisch begrenzen", "Drehzahlbegrenzer einbauen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M013", ReductionType: "design", SubType: "force_energy", Name: "Kraft begrenzen", Description: "Die maximal auftretende Kraft wird konstruktiv so begrenzt, dass keine Verletzungsgefahr besteht.", HazardCategory: "mechanical", Examples: []string{"Federbelastete Kraftbegrenzung einsetzen", "Antriebsdrehmoment begrenzen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2", "ISO/TS 15066"}}, - {ID: "M014", ReductionType: "design", SubType: "force_energy", Name: "Kinematik aendern", Description: "Bewegungsart oder -richtung wird umgestaltet, sodass die Gefaehrdung entfaellt.", HazardCategory: "mechanical", Examples: []string{"Linearbewegung statt Rotation einsetzen", "Bewegungsrichtung von Bedienerseite wegfuehren"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M015", ReductionType: "design", SubType: "force_energy", Name: "Gewicht reduzieren", Description: "Gewicht beweglicher Maschinenteile wird minimiert zur Verringerung der Verletzungsschwere.", HazardCategory: "mechanical", Examples: []string{"Leichtbauwerkstoffe fuer bewegliche Arme", "Hohlprofile statt Vollmaterial"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M016", ReductionType: "design", SubType: "force_energy", Name: "Redundante Konstruktion", Description: "Sicherheitskritische Bauteile sind mehrfach ausgefuehrt fuer Ausfallsicherheit.", HazardCategory: "mechanical", Examples: []string{"Doppelte Tragseile an Hebezeugen", "Redundante Bremssysteme vorsehen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.3", "ISO 13849-1"}}, - {ID: "M017", ReductionType: "design", SubType: "force_energy", Name: "Mechanische Begrenzung", Description: "Feste mechanische Anschlaege begrenzen den Bewegungsbereich.", HazardCategory: "mechanical", Examples: []string{"Feste Endanschlaege an Linearachsen", "Drehwinkelbegrenzung an Drehachsen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M018", ReductionType: "design", SubType: "force_energy", Name: "Schwerkraftsichere Konstruktion", Description: "Konstruktion verhindert unkontrollierte Bewegung durch Schwerkraft bei Energieausfall.", HazardCategory: "mechanical", Examples: []string{"Lasthalteventile in Hubzylindern", "Federspeicherbremsen an Vertikalachsen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.3", "EN 693"}}, - {ID: "M019", ReductionType: "design", SubType: "force_energy", Name: "Energiebegrenzung", Description: "Die gesamt verfuegbare Energie im System wird konstruktiv auf ein sicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Kleine Pneumatikzylinder statt grosser verwenden", "Niedrigdruck-Hydraulik einsetzen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M020", ReductionType: "design", SubType: "force_energy", Name: "Sichere Energieuebertragung", Description: "Energieleitungen werden so verlegt, dass Leckagen oder Brueche keine Gefaehrdung darstellen.", HazardCategory: "electrical", Examples: []string{"Schleppketten fuer flexible Leitungen", "Doppelwandige Druckleitungen verwenden"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.9"}}, - {ID: "M021", ReductionType: "design", SubType: "force_energy", Name: "Nachgiebige Elemente", Description: "Maschinenteile im Kontaktbereich werden nachgiebig gestaltet zur Verletzungsminimierung.", HazardCategory: "mechanical", Examples: []string{"Polsterungen an Klemmpunkten", "Federnd gelagerte Anschlaege"}, NormReferences: []string{"ISO/TS 15066", "ISO 12100:2010 Kap. 6.2.2.2"}}, - {ID: "M022", ReductionType: "design", SubType: "force_energy", Name: "Sichere Kraftuebertragung", Description: "Kraftuebertragungselemente sind gesichert gegen Bruch oder Loesen.", HazardCategory: "mechanical", Examples: []string{"Wellensicherungen gegen Axialverschiebung", "Sicherheitswellen mit Sollbruchstelle"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, + {ID: "M011", ReductionType: "design", SubType: "force_energy", Name: "Bewegungsenergie reduzieren", Description: "Kinetische Energie beweglicher Maschinenteile wird auf ein sicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Masse beweglicher Teile verringern", "Hublaenge verkuerzen"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M012", ReductionType: "design", SubType: "force_energy", Name: "Geschwindigkeit reduzieren", Description: "Verfahrgeschwindigkeit wird konstruktiv auf ein verletzungssicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Maximale Achsgeschwindigkeit mechanisch begrenzen", "Drehzahlbegrenzer einbauen"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M013", ReductionType: "design", SubType: "force_energy", Name: "Kraft begrenzen", Description: "Die maximal auftretende Kraft wird konstruktiv so begrenzt, dass keine Verletzungsgefahr besteht.", HazardCategory: "mechanical", Examples: []string{"Federbelastete Kraftbegrenzung einsetzen", "Antriebsdrehmoment begrenzen"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten", "ISO/TS 15066"}}, + {ID: "M014", ReductionType: "design", SubType: "force_energy", Name: "Kinematik aendern", Description: "Bewegungsart oder -richtung wird umgestaltet, sodass die Gefaehrdung entfaellt.", HazardCategory: "mechanical", Examples: []string{"Linearbewegung statt Rotation einsetzen", "Bewegungsrichtung von Bedienerseite wegfuehren"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M015", ReductionType: "design", SubType: "force_energy", Name: "Gewicht reduzieren", Description: "Gewicht beweglicher Maschinenteile wird minimiert zur Verringerung der Verletzungsschwere.", HazardCategory: "mechanical", Examples: []string{"Leichtbauwerkstoffe fuer bewegliche Arme", "Hohlprofile statt Vollmaterial"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M016", ReductionType: "design", SubType: "force_energy", Name: "Redundante Konstruktion", Description: "Sicherheitskritische Bauteile sind mehrfach ausgefuehrt fuer Ausfallsicherheit.", HazardCategory: "mechanical", Examples: []string{"Doppelte Tragseile an Hebezeugen", "Redundante Bremssysteme vorsehen"}, NormReferences: []string{"ISO 12100 — Allgemeine technische Kenntnisse", "ISO 13849-1"}}, + {ID: "M017", ReductionType: "design", SubType: "force_energy", Name: "Mechanische Begrenzung", Description: "Feste mechanische Anschlaege begrenzen den Bewegungsbereich.", HazardCategory: "mechanical", Examples: []string{"Feste Endanschlaege an Linearachsen", "Drehwinkelbegrenzung an Drehachsen"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M018", ReductionType: "design", SubType: "force_energy", Name: "Schwerkraftsichere Konstruktion", Description: "Konstruktion verhindert unkontrollierte Bewegung durch Schwerkraft bei Energieausfall.", HazardCategory: "mechanical", Examples: []string{"Lasthalteventile in Hubzylindern", "Federspeicherbremsen an Vertikalachsen"}, NormReferences: []string{"ISO 12100 — Allgemeine technische Kenntnisse", "EN 693"}}, + {ID: "M019", ReductionType: "design", SubType: "force_energy", Name: "Energiebegrenzung", Description: "Die gesamt verfuegbare Energie im System wird konstruktiv auf ein sicheres Niveau begrenzt.", HazardCategory: "mechanical", Examples: []string{"Kleine Pneumatikzylinder statt grosser verwenden", "Niedrigdruck-Hydraulik einsetzen"}, NormReferences: []string{"ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M020", ReductionType: "design", SubType: "force_energy", Name: "Sichere Energieuebertragung", Description: "Energieleitungen werden so verlegt, dass Leckagen oder Brueche keine Gefaehrdung darstellen.", HazardCategory: "electrical", Examples: []string{"Schleppketten fuer flexible Leitungen", "Doppelwandige Druckleitungen verwenden"}, NormReferences: []string{"ISO 12100 — Minimierung Fehlerwahrscheinlichkeit"}}, + {ID: "M021", ReductionType: "design", SubType: "force_energy", Name: "Nachgiebige Elemente", Description: "Maschinenteile im Kontaktbereich werden nachgiebig gestaltet zur Verletzungsminimierung.", HazardCategory: "mechanical", Examples: []string{"Polsterungen an Klemmpunkten", "Federnd gelagerte Anschlaege"}, NormReferences: []string{"ISO/TS 15066", "ISO 12100 — Physikalische Kenndaten"}}, + {ID: "M022", ReductionType: "design", SubType: "force_energy", Name: "Sichere Kraftuebertragung", Description: "Kraftuebertragungselemente sind gesichert gegen Bruch oder Loesen.", HazardCategory: "mechanical", Examples: []string{"Wellensicherungen gegen Axialverschiebung", "Sicherheitswellen mit Sollbruchstelle"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, // ── Material (M023-M028) ──────────────────────────────────────────── - {ID: "M023", ReductionType: "design", SubType: "material", Name: "Sichere Materialwahl", Description: "Werkstoffe werden so gewaehlt, dass sie keine zusaetzlichen Gefaehrdungen verursachen.", HazardCategory: "material_environmental", Examples: []string{"Nicht-toxische Kunststoffe waehlen", "Korrosionsbestaendige Legierungen einsetzen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, - {ID: "M024", ReductionType: "design", SubType: "material", Name: "Stabile Konstruktion", Description: "Konstruktion auf ausreichende Festigkeit ausgelegt gegen strukturelles Versagen.", HazardCategory: "mechanical", Examples: []string{"Sicherheitsfaktoren bei Tragstrukturen", "Dauerfestigkeit der Schweissnaehte pruefen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.3", "EN 1993-1"}}, - {ID: "M025", ReductionType: "design", SubType: "material", Name: "Splitterschutzglas", Description: "Sicherheitsglas verhindert Verletzungen durch Splitter bei Glasbruch.", HazardCategory: "mechanical", Examples: []string{"Verbundsicherheitsglas fuer Schutzhauben", "Polycarbonat-Scheiben an Drehmaschinen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1", "ISO 14120"}}, - {ID: "M026", ReductionType: "design", SubType: "material", Name: "Korrosionsbestaendige Materialien", Description: "Korrosionsfeste Werkstoffe verhindern Festigkeitsverlust und damit Versagen.", HazardCategory: "material_environmental", Examples: []string{"Edelstahl fuer feuchte Umgebungen", "Beschichtete Bauteile in chemischer Umgebung"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, - {ID: "M027", ReductionType: "design", SubType: "material", Name: "Ausbrecharme Materialien", Description: "Werkstoffe, die bei Bruch keine scharfen Splitter erzeugen.", HazardCategory: "mechanical", Examples: []string{"Duktile Gusswerkstoffe statt sproeder", "Faserverstaerkte Kunststoffe statt Glas"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.2.1"}}, - {ID: "M028", ReductionType: "design", SubType: "material", Name: "Brandbestaendige Materialien", Description: "Feuerfeste Werkstoffe an brandgefaehrdeten Stellen minimieren Brandgefahr.", HazardCategory: "thermal", Examples: []string{"Flammhemmende Kabelisolierung", "Feuerfeste Hydraulikfluessigkeiten"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.4", "EN 13501-1"}}, + {ID: "M023", ReductionType: "design", SubType: "material", Name: "Sichere Materialwahl", Description: "Werkstoffe werden so gewaehlt, dass sie keine zusaetzlichen Gefaehrdungen verursachen.", HazardCategory: "material_environmental", Examples: []string{"Nicht-toxische Kunststoffe waehlen", "Korrosionsbestaendige Legierungen einsetzen"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, + {ID: "M024", ReductionType: "design", SubType: "material", Name: "Stabile Konstruktion", Description: "Konstruktion auf ausreichende Festigkeit ausgelegt gegen strukturelles Versagen.", HazardCategory: "mechanical", Examples: []string{"Sicherheitsfaktoren bei Tragstrukturen", "Dauerfestigkeit der Schweissnaehte pruefen"}, NormReferences: []string{"ISO 12100 — Allgemeine technische Kenntnisse", "EN 1993-1"}}, + {ID: "M025", ReductionType: "design", SubType: "material", Name: "Splitterschutzglas", Description: "Sicherheitsglas verhindert Verletzungen durch Splitter bei Glasbruch.", HazardCategory: "mechanical", Examples: []string{"Verbundsicherheitsglas fuer Schutzhauben", "Polycarbonat-Scheiben an Drehmaschinen"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung", "ISO 14120"}}, + {ID: "M026", ReductionType: "design", SubType: "material", Name: "Korrosionsbestaendige Materialien", Description: "Korrosionsfeste Werkstoffe verhindern Festigkeitsverlust und damit Versagen.", HazardCategory: "material_environmental", Examples: []string{"Edelstahl fuer feuchte Umgebungen", "Beschichtete Bauteile in chemischer Umgebung"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, + {ID: "M027", ReductionType: "design", SubType: "material", Name: "Ausbrecharme Materialien", Description: "Werkstoffe, die bei Bruch keine scharfen Splitter erzeugen.", HazardCategory: "mechanical", Examples: []string{"Duktile Gusswerkstoffe statt sproeder", "Faserverstaerkte Kunststoffe statt Glas"}, NormReferences: []string{"ISO 12100 — Geometrie und Anordnung"}}, + {ID: "M028", ReductionType: "design", SubType: "material", Name: "Brandbestaendige Materialien", Description: "Feuerfeste Werkstoffe an brandgefaehrdeten Stellen minimieren Brandgefahr.", HazardCategory: "thermal", Examples: []string{"Flammhemmende Kabelisolierung", "Feuerfeste Hydraulikfluessigkeiten"}, NormReferences: []string{"ISO 12100 — Ergonomische Grundsaetze", "EN 13501-1"}}, // ── Ergonomics (M029-M038) ────────────────────────────────────────── - {ID: "M029", ReductionType: "design", SubType: "ergonomics", Name: "Ergonomische Arbeitshoehe", Description: "Arbeitshoehe ist an Bedienergroesse anpassbar fuer belastungsarmes Arbeiten.", HazardCategory: "ergonomic", Examples: []string{"Hoehenverstellbare Arbeitstische", "Bedienfeld auf Ellbogenhoehe"}, NormReferences: []string{"EN 614-1", "ISO 12100:2010 Kap. 6.2.8"}}, + {ID: "M029", ReductionType: "design", SubType: "ergonomics", Name: "Ergonomische Arbeitshoehe", Description: "Arbeitshoehe ist an Bedienergroesse anpassbar fuer belastungsarmes Arbeiten.", HazardCategory: "ergonomic", Examples: []string{"Hoehenverstellbare Arbeitstische", "Bedienfeld auf Ellbogenhoehe"}, NormReferences: []string{"EN 614-1", "ISO 12100 — Elektrische Energieversorgung"}}, {ID: "M030", ReductionType: "design", SubType: "ergonomics", Name: "Greifraum-Optimierung", Description: "Bedienelemente sind in ergonomisch guenstiger Reichweite platziert.", HazardCategory: "ergonomic", Examples: []string{"Haeufig genutzte Taster im Nahbereich", "Reichweitendiagramme bei der Planung anwenden"}, NormReferences: []string{"EN 614-1", "EN 894-3"}}, {ID: "M031", ReductionType: "design", SubType: "ergonomics", Name: "Gewichtsreduzierung Handhabung", Description: "Gewicht von Handwerkzeugen und Handhabungsteilen unter ergonomischen Grenzwerten.", HazardCategory: "ergonomic", Examples: []string{"Gewicht von Handwerkzeugen unter 2,5 kg", "Hebevorrichtungen fuer schwere Teile"}, NormReferences: []string{"EN 1005-2", "ISO 11228-1"}}, {ID: "M032", ReductionType: "design", SubType: "ergonomics", Name: "Intuitive Bedienoberflaeche", Description: "Bedienelemente und Anzeigen sind logisch angeordnet gegen Fehlbedienung.", HazardCategory: "ergonomic", Examples: []string{"Einheitliche Farbcodierung", "Logische Anordnung der Bedienelemente"}, NormReferences: []string{"EN 894-1", "EN 894-2", "EN 894-3"}}, - {ID: "M033", ReductionType: "design", SubType: "ergonomics", Name: "Gute Sichtbarkeit", Description: "Sicherheitsrelevante Bereiche sind vom Bedienstandort einsehbar.", HazardCategory: "ergonomic", Examples: []string{"Transparente Schutzhauben verwenden", "Kamerabasierte Sichthilfen installieren"}, NormReferences: []string{"EN 614-1", "ISO 12100:2010 Kap. 6.2.8"}}, + {ID: "M033", ReductionType: "design", SubType: "ergonomics", Name: "Gute Sichtbarkeit", Description: "Sicherheitsrelevante Bereiche sind vom Bedienstandort einsehbar.", HazardCategory: "ergonomic", Examples: []string{"Transparente Schutzhauben verwenden", "Kamerabasierte Sichthilfen installieren"}, NormReferences: []string{"EN 614-1", "ISO 12100 — Elektrische Energieversorgung"}}, {ID: "M034", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Mensch-Maschine-Interaktion", Description: "Schnittstelle Bediener/Maschine schliesst gefaehrliche Missverstaendnisse aus.", HazardCategory: "ergonomic", Examples: []string{"Eindeutige Statusindikatoren", "Bestaetigung vor kritischen Befehlen"}, NormReferences: []string{"EN 894-1", "IEC 60447"}}, - {ID: "M035", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Wartungszugaenge", Description: "Wartungsbereiche sind gefahrlos zugaenglich ohne Demontage von Schutzeinrichtungen.", HazardCategory: "ergonomic", Examples: []string{"Wartungsklappen mit Sicherheitsverriegelung", "Ausreichende Arbeitsflaeche im Wartungsbereich"}, NormReferences: []string{"EN 547-3", "ISO 12100:2010 Kap. 6.2.8"}}, - {ID: "M036", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Montagepunkte", Description: "Montagepunkte fuer sicheres Handling waehrend Montage und Demontage.", HazardCategory: "ergonomic", Examples: []string{"Anschlagpunkte fuer Hebezeuge", "Passstifte fuer lagegenaue Montage"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.8"}}, + {ID: "M035", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Wartungszugaenge", Description: "Wartungsbereiche sind gefahrlos zugaenglich ohne Demontage von Schutzeinrichtungen.", HazardCategory: "ergonomic", Examples: []string{"Wartungsklappen mit Sicherheitsverriegelung", "Ausreichende Arbeitsflaeche im Wartungsbereich"}, NormReferences: []string{"EN 547-3", "ISO 12100 — Elektrische Energieversorgung"}}, + {ID: "M036", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Montagepunkte", Description: "Montagepunkte fuer sicheres Handling waehrend Montage und Demontage.", HazardCategory: "ergonomic", Examples: []string{"Anschlagpunkte fuer Hebezeuge", "Passstifte fuer lagegenaue Montage"}, NormReferences: []string{"ISO 12100 — Elektrische Energieversorgung"}}, {ID: "M037", ReductionType: "design", SubType: "ergonomics", Name: "Sichere Servicezugaenge", Description: "Servicebereiche sind bei abgeschalteter Maschine sicher zugaenglich.", HazardCategory: "ergonomic", Examples: []string{"Servicetreppen und -plattformen", "Beleuchtung im Servicebereich"}, NormReferences: []string{"EN 547-3", "ISO 14122-3"}}, {ID: "M038", ReductionType: "design", SubType: "ergonomics", Name: "Vibrationsarme Konstruktion", Description: "Vibrationen und Koerperschall werden an der Quelle minimiert.", HazardCategory: "noise_vibration", Examples: []string{"Schwingungsdaempfer an Motoren", "Elastische Maschinenlagerung"}, NormReferences: []string{"ISO 5349-1", "EN 1032"}}, @@ -68,23 +68,23 @@ func getDesignMeasures() []ProtectiveMeasureEntry { {ID: "M039", ReductionType: "design", SubType: "control_design", Name: "Sichere Software-Fallbacks", Description: "Steuerungssoftware enthaelt Rueckfallstrategien fuer sichere Zustaende bei Fehlern.", HazardCategory: "software_control", Examples: []string{"Standardwerte bei Sensorausfall", "Sicherer Stopp bei unplausiblen Daten"}, NormReferences: []string{"IEC 62443-4-1", "ISO 13849-1"}}, {ID: "M040", ReductionType: "design", SubType: "control_design", Name: "Deterministische Steuerungslogik", Description: "Steuerungslogik erzeugt bei identischen Eingaben immer identische Ausgaben.", HazardCategory: "software_control", Examples: []string{"Keine Zufallselemente in Sicherheitsfunktionen", "Feste Zykluszeiten fuer Safety-Tasks"}, NormReferences: []string{"IEC 61508-3", "IEC 62443-4-1"}}, {ID: "M041", ReductionType: "design", SubType: "control_design", Name: "Definierte Zustandsmaschine", Description: "Alle Maschinenzustaende und Uebergaenge sind vollstaendig definiert und abgesichert.", HazardCategory: "software_control", Examples: []string{"Zustandsdiagramm erstellen", "Ungueltige Uebergaenge softwareseitig blockieren"}, NormReferences: []string{"IEC 61508-3", "ISO 13849-1"}}, - {ID: "M042", ReductionType: "design", SubType: "control_design", Name: "Sichere Restart-Logik", Description: "Neustart nur durch bewusste Bedienerhandlung, kein automatischer Wiederanlauf.", HazardCategory: "software_control", Examples: []string{"Automatischen Wiederanlauf nach Netzausfall verhindern", "Quittierungspflicht vor Neustart"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.11.4", "IEC 60204-1"}}, + {ID: "M042", ReductionType: "design", SubType: "control_design", Name: "Sichere Restart-Logik", Description: "Neustart nur durch bewusste Bedienerhandlung, kein automatischer Wiederanlauf.", HazardCategory: "software_control", Examples: []string{"Automatischen Wiederanlauf nach Netzausfall verhindern", "Quittierungspflicht vor Neustart"}, NormReferences: []string{"ISO 12100 — Stillsetzen im Notfall", "IEC 60204-1"}}, {ID: "M043", ReductionType: "design", SubType: "control_design", Name: "Sichere Fehlermodi", Description: "Jeder erkannte Fehler fuehrt automatisch in einen vordefinierten sicheren Zustand.", HazardCategory: "software_control", Examples: []string{"Fail-Safe bei Sensorausfall", "Fehlerkatalog mit sicheren Zustaenden"}, NormReferences: []string{"ISO 13849-1", "IEC 62061"}}, {ID: "M044", ReductionType: "design", SubType: "control_design", Name: "Zweikanalige Steuerung", Description: "Sicherheitsfunktionen werden ueber zwei unabhaengige Kanaele ausgefuehrt.", HazardCategory: "software_control", Examples: []string{"Kategorie-3/4-Architektur nach ISO 13849", "Zwei getrennte Abschaltpfade"}, NormReferences: []string{"ISO 13849-1", "IEC 62061"}}, {ID: "M045", ReductionType: "design", SubType: "control_design", Name: "Steuerungstechnische Sicherheit", Description: "Steuerungsarchitektur ist auf Fehlererkennung und sichere Reaktion ausgelegt.", HazardCategory: "software_control", Examples: []string{"Cross-Monitoring zwischen Kanaelen", "Diversitaere Signalverarbeitung"}, NormReferences: []string{"ISO 13849-1", "IEC 61508"}}, - {ID: "M046", ReductionType: "design", SubType: "control_design", Name: "Sichere Energieabschaltung", Description: "Maschine kann jederzeit sicher von allen Energiequellen getrennt werden.", HazardCategory: "electrical", Examples: []string{"Hauptschalter mit Absperrmoeglichkeit", "Pneumatik-Absperrventil am Eingang"}, NormReferences: []string{"IEC 60204-1", "ISO 12100:2010 Kap. 6.2.10"}}, - {ID: "M047", ReductionType: "design", SubType: "control_design", Name: "Sichere Energieentladung", Description: "Alle gespeicherten Energien werden nach Abschalten kontrolliert abgebaut.", HazardCategory: "electrical", Examples: []string{"Kondensatoren ueber Entladewiderstaende", "Druckspeicher ueber Entlastungsventil"}, NormReferences: []string{"IEC 60204-1 Kap. 5.4", "ISO 12100:2010 Kap. 6.2.10"}}, - {ID: "M048", ReductionType: "design", SubType: "control_design", Name: "Sichere Notzustaende", Description: "Fuer alle Notsituationen sind definierte Zustaende festgelegt.", HazardCategory: "general", Examples: []string{"Not-Halt-Zustand mit definierten Positionen", "Evakuierungszustand mit geoeffneten Schutztoren"}, NormReferences: []string{"ISO 13850", "IEC 60204-1 Kap. 9.2.5.4"}}, - {ID: "M049", ReductionType: "design", SubType: "control_design", Name: "Sichere Betriebsartenwahl", Description: "Umschaltung zwischen Betriebsarten ist abgesichert und nur kontrolliert moeglich.", HazardCategory: "software_control", Examples: []string{"Schluesselschalter fuer Betriebsarten", "Sichere Betriebsartenerkennung in SPS"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.11.10", "IEC 60204-1"}}, - {ID: "M050", ReductionType: "design", SubType: "control_design", Name: "Sicherer Anlauf nach Stoerung", Description: "Wiederanlauf nach Stoerung folgt definierter Prozedur mit Bedienerfreigabe.", HazardCategory: "software_control", Examples: []string{"Schrittweiser Anlauf nach Fehler", "Pruefsequenz vor Produktionsfreigabe"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.11.4", "IEC 60204-1"}}, + {ID: "M046", ReductionType: "design", SubType: "control_design", Name: "Sichere Energieabschaltung", Description: "Maschine kann jederzeit sicher von allen Energiequellen getrennt werden.", HazardCategory: "electrical", Examples: []string{"Hauptschalter mit Absperrmoeglichkeit", "Pneumatik-Absperrventil am Eingang"}, NormReferences: []string{"IEC 60204-1", "ISO 12100 — Automatisierungstechnik"}}, + {ID: "M047", ReductionType: "design", SubType: "control_design", Name: "Sichere Energieentladung", Description: "Alle gespeicherten Energien werden nach Abschalten kontrolliert abgebaut.", HazardCategory: "electrical", Examples: []string{"Kondensatoren ueber Entladewiderstaende", "Druckspeicher ueber Entlastungsventil"}, NormReferences: []string{"IEC 60204-1 — Trennen und Ausschalten", "ISO 12100 — Automatisierungstechnik"}}, + {ID: "M048", ReductionType: "design", SubType: "control_design", Name: "Sichere Notzustaende", Description: "Fuer alle Notsituationen sind definierte Zustaende festgelegt.", HazardCategory: "general", Examples: []string{"Not-Halt-Zustand mit definierten Positionen", "Evakuierungszustand mit geoeffneten Schutztoren"}, NormReferences: []string{"ISO 13850", "IEC 60204-1 — Not-Halt-Steuerung"}}, + {ID: "M049", ReductionType: "design", SubType: "control_design", Name: "Sichere Betriebsartenwahl", Description: "Umschaltung zwischen Betriebsarten ist abgesichert und nur kontrolliert moeglich.", HazardCategory: "software_control", Examples: []string{"Schluesselschalter fuer Betriebsarten", "Sichere Betriebsartenerkennung in SPS"}, NormReferences: []string{"ISO 12100 — Sicherheitsbezogene Steuerungssysteme0", "IEC 60204-1"}}, + {ID: "M050", ReductionType: "design", SubType: "control_design", Name: "Sicherer Anlauf nach Stoerung", Description: "Wiederanlauf nach Stoerung folgt definierter Prozedur mit Bedienerfreigabe.", HazardCategory: "software_control", Examples: []string{"Schrittweiser Anlauf nach Fehler", "Pruefsequenz vor Produktionsfreigabe"}, NormReferences: []string{"ISO 12100 — Stillsetzen im Notfall", "IEC 60204-1"}}, // ── Fluid Design (M051-M058) ──────────────────────────────────────── - {ID: "M051", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Hydraulikdimensionierung", Description: "Hydrauliksystem so ausgelegt, dass Druckspitzen keine unkontrollierten Bewegungen verursachen.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Druckspeicher mit Berstscheibe", "Hydraulikleitungen druckfest dimensioniert"}, NormReferences: []string{"ISO 4413", "ISO 12100:2010 Kap. 6.2.9"}}, - {ID: "M052", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Pneumatikdimensionierung", Description: "Pneumatik so ausgelegt, dass Druckverlust zu sicherem Zustand fuehrt.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Federrueckstellung bei Druckausfall", "Druckbegrenzer in Versorgungsleitungen"}, NormReferences: []string{"ISO 4414", "ISO 12100:2010 Kap. 6.2.9"}}, + {ID: "M051", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Hydraulikdimensionierung", Description: "Hydrauliksystem so ausgelegt, dass Druckspitzen keine unkontrollierten Bewegungen verursachen.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Druckspeicher mit Berstscheibe", "Hydraulikleitungen druckfest dimensioniert"}, NormReferences: []string{"ISO 4413", "ISO 12100 — Minimierung Fehlerwahrscheinlichkeit"}}, + {ID: "M052", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Pneumatikdimensionierung", Description: "Pneumatik so ausgelegt, dass Druckverlust zu sicherem Zustand fuehrt.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Federrueckstellung bei Druckausfall", "Druckbegrenzer in Versorgungsleitungen"}, NormReferences: []string{"ISO 4414", "ISO 12100 — Minimierung Fehlerwahrscheinlichkeit"}}, {ID: "M053", ReductionType: "design", SubType: "fluid_design", Name: "Druckbegrenzung", Description: "Passive Druckbegrenzung verhindert Ueberschreiten des zulaessigen Drucks.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Berstscheiben an Druckbehaeltern", "Ueberdruckventile in Hydraulikkreisen"}, NormReferences: []string{"ISO 4413", "EN 764-7"}}, - {ID: "M054", ReductionType: "design", SubType: "fluid_design", Name: "Sichere thermische Auslegung", Description: "Thermische Belastung beruecksichtigt zur Vermeidung von Ueberhitzung und Brandgefahr.", HazardCategory: "thermal", Examples: []string{"Waermeableitung durch Kuehlrippen", "Brandlast durch Materialwahl minimieren"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.4"}}, - {ID: "M055", ReductionType: "design", SubType: "fluid_design", Name: "Temperaturbegrenzung", Description: "Zugaengliche Oberflaechen erreichen keine gefaehrlichen Temperaturen.", HazardCategory: "thermal", Examples: []string{"Oberflaechentemperatur unter 43 Grad Celsius", "Thermische Abschirmungen an Oefen"}, NormReferences: []string{"EN ISO 13732-1", "ISO 12100:2010 Kap. 6.2.4"}}, - {ID: "M056", ReductionType: "design", SubType: "fluid_design", Name: "Passive Kuehlung", Description: "Waermeabfuhr ohne aktive Komponenten verhindert Kuehlungsausfall.", HazardCategory: "thermal", Examples: []string{"Natuerliche Konvektion durch Rippendesign", "Waermeleitrohre (Heatpipes) einsetzen"}, NormReferences: []string{"ISO 12100:2010 Kap. 6.2.4"}}, + {ID: "M054", ReductionType: "design", SubType: "fluid_design", Name: "Sichere thermische Auslegung", Description: "Thermische Belastung beruecksichtigt zur Vermeidung von Ueberhitzung und Brandgefahr.", HazardCategory: "thermal", Examples: []string{"Waermeableitung durch Kuehlrippen", "Brandlast durch Materialwahl minimieren"}, NormReferences: []string{"ISO 12100 — Ergonomische Grundsaetze"}}, + {ID: "M055", ReductionType: "design", SubType: "fluid_design", Name: "Temperaturbegrenzung", Description: "Zugaengliche Oberflaechen erreichen keine gefaehrlichen Temperaturen.", HazardCategory: "thermal", Examples: []string{"Oberflaechentemperatur unter 43 Grad Celsius", "Thermische Abschirmungen an Oefen"}, NormReferences: []string{"EN ISO 13732-1", "ISO 12100 — Ergonomische Grundsaetze"}}, + {ID: "M056", ReductionType: "design", SubType: "fluid_design", Name: "Passive Kuehlung", Description: "Waermeabfuhr ohne aktive Komponenten verhindert Kuehlungsausfall.", HazardCategory: "thermal", Examples: []string{"Natuerliche Konvektion durch Rippendesign", "Waermeleitrohre (Heatpipes) einsetzen"}, NormReferences: []string{"ISO 12100 — Ergonomische Grundsaetze"}}, {ID: "M057", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Leitungsfuehrung", Description: "Hydraulik-/Pneumatikleitungen sind vor Beschaedigung geschuetzt.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Leitungen in geschuetzten Kanaelen", "Farbcodierte Leitungen"}, NormReferences: []string{"ISO 4413", "ISO 4414"}}, {ID: "M058", ReductionType: "design", SubType: "fluid_design", Name: "Sichere Entlueftung", Description: "Entlueftungssysteme stellen sicheren Druckabbau bei Wartung und Notfall sicher.", HazardCategory: "pneumatic_hydraulic", Examples: []string{"Entlueftungspunkte an Druckbehaeltern", "Manuelle Entlueftung mit Sicherheitsventil"}, NormReferences: []string{"ISO 4413", "ISO 4414"}}, diff --git a/ai-compliance-sdk/internal/iace/measures_library_ext.go b/ai-compliance-sdk/internal/iace/measures_library_ext.go index f9e21c0..7ca212c 100644 --- a/ai-compliance-sdk/internal/iace/measures_library_ext.go +++ b/ai-compliance-sdk/internal/iace/measures_library_ext.go @@ -189,5 +189,37 @@ func getInformationMeasures() []ProtectiveMeasureEntry { {ID: "M198", ReductionType: "information", SubType: "organizational", Name: "Wartungscheckliste", Description: "Systematische Abarbeitung aller Wartungspunkte mit Dokumentation.", HazardCategory: "general", Examples: []string{"Checkliste woechentliche Wartung", "Oelstand und Filter pruefen"}, NormReferences: []string{"BetrSichV §10"}}, {ID: "M199", ReductionType: "information", SubType: "organizational", Name: "Schichtwechsel-Checkliste", Description: "Checkliste fuer sichere Uebergabe zwischen Schichten.", HazardCategory: "general", Examples: []string{"Maschinenzustand dokumentieren", "Offene Stoerungen uebergeben"}, NormReferences: []string{"DGUV Vorschrift 1"}}, {ID: "M200", ReductionType: "information", SubType: "organizational", Name: "Cyber-Security-Hinweise fuer Betreiber", Description: "Hinweise zum Schutz der Maschinensteuerung vor Cyberangriffen.", HazardCategory: "cyber_network", Examples: []string{"Netzwerksicherheitshinweise", "Regelmaessige Aktualisierung empfehlen"}, NormReferences: []string{"IEC 62443-2-1", "VDI/VDE 2182"}}, + + // ══════════════════════════════════════════════════════════════════ + // Phase 1B: Neue Massnahmen fuer bisher unabgedeckte Kategorien + // ══════════════════════════════════════════════════════════════════ + + // ── Kommunikationsausfall (communication_failure) ──────────────── + {ID: "M201", ReductionType: "design", SubType: "control_design", Name: "Kommunikationsredundanz", Description: "Sicherheitskritische Kommunikationspfade werden redundant ausgefuehrt, so dass der Ausfall eines Kanals nicht zum Verlust der Sicherheitsfunktion fuehrt.", HazardCategory: "cyber_network", Examples: []string{"Dual-Channel Safety-Bus", "Redundante Ethernet-Verbindung"}, NormReferences: []string{"IEC 62443 — Netzwerk-Redundanz", "ISO 13849-1 — Redundanz-Architektur"}}, + {ID: "M202", ReductionType: "design", SubType: "control_design", Name: "Kommunikations-Timeout mit sicherem Zustand", Description: "Bei Ausbleiben erwarteter Nachrichten innerhalb definierter Zeitfenster wird automatisch ein sicherer Zustand hergestellt.", HazardCategory: "cyber_network", Examples: []string{"Watchdog-Timeout auf Safety-Bus", "Heartbeat-Ueberwachung zwischen SPS und Antrieb"}, NormReferences: []string{"IEC 61784 — Feldbussicherheit", "ISO 13849-1 — Fehlererkennung"}}, + {ID: "M203", ReductionType: "design", SubType: "control_design", Name: "Fallback-Betrieb bei Kommunikationsverlust", Description: "Ein definierter Notbetriebsmodus stellt grundlegende Sicherheitsfunktionen auch ohne aktive Kommunikation sicher.", HazardCategory: "cyber_network", Examples: []string{"Lokale Sicherheitssteuerung bei Busverlust", "Autonomer Not-Halt ohne Netzwerk"}, NormReferences: []string{"IEC 62443 — Ausfallsicherheit", "ISO 12100 — Inhaerent sichere Konstruktion"}}, + + // ── HMI-Fehler (hmi_error) ────────────────────────────────────── + {ID: "M204", ReductionType: "design", SubType: "control_design", Name: "HMI-Usability-Pruefung", Description: "Systematische Pruefung der Benutzeroberflaeche auf Fehlbedienungspotenzial, insbesondere fuer sicherheitskritische Bedienhandlungen.", HazardCategory: "software_control", Examples: []string{"Usability-Test mit Bedienpersonal", "FMEA der Bedienoberflaeche"}, NormReferences: []string{"ISO 12100 — Ergonomische Grundsaetze", "EN 894 — Ergonomische Anforderungen an Anzeigen und Stellteile"}}, + {ID: "M205", ReductionType: "design", SubType: "control_design", Name: "Eindeutiges visuelles Feedback", Description: "Jede sicherheitsrelevante Zustandsaenderung wird dem Bediener durch eindeutige visuelle, akustische oder haptische Rueckmeldung angezeigt.", HazardCategory: "software_control", Examples: []string{"Ampelsignal fuer Maschinenzustand", "Akustisches Signal bei Betriebsartwechsel"}, NormReferences: []string{"IEC 60204-1 — Anzeigeelemente", "ISO 7731 — Gefahrensignale"}}, + {ID: "M206", ReductionType: "design", SubType: "control_design", Name: "Betriebsarten-Anzeige mit Bestaetigung", Description: "Die aktive Betriebsart wird jederzeit eindeutig angezeigt. Wechsel sicherheitskritischer Betriebsarten erfordern aktive Bestaetigung.", HazardCategory: "software_control", Examples: []string{"Schluesselschalter fuer Betriebsartwechsel", "Quittierungspflicht bei Teach-Mode"}, NormReferences: []string{"ISO 12100 — Betriebsarten", "IEC 60204-1 — Betriebsartenwahl"}}, + + // ── Firmware-Korruption (firmware_corruption) ──────────────────── + {ID: "M207", ReductionType: "design", SubType: "control_design", Name: "Secure Boot und Firmware-Integritaetspruefung", Description: "Beim Systemstart wird die Integritaet der Firmware kryptografisch geprueft. Manipulierte oder beschaedigte Firmware wird nicht ausgefuehrt.", HazardCategory: "cyber_network", Examples: []string{"Hash-Verifikation beim Boot", "Signierte Firmware-Images"}, NormReferences: []string{"IEC 62443 — Integritaetspruefung", "EU CRA — Software-Sicherheit"}}, + {ID: "M208", ReductionType: "design", SubType: "control_design", Name: "Signierte Firmware-Updates", Description: "Firmware-Updates werden nur akzeptiert wenn sie kryptografisch signiert und verifiziert sind. Unsignierte Updates werden abgelehnt.", HazardCategory: "cyber_network", Examples: []string{"RSA/ECDSA-signierte Update-Pakete", "Zertifikatsbasierte Verifikation"}, NormReferences: []string{"IEC 62443 — Patch-Management", "EU CRA — Update-Sicherheit"}}, + {ID: "M209", ReductionType: "design", SubType: "control_design", Name: "Firmware-Rollback-Mechanismus", Description: "Bei fehlgeschlagenem Update kann automatisch auf die letzte funktionierende Firmware-Version zurueckgesetzt werden.", HazardCategory: "cyber_network", Examples: []string{"A/B-Partition-Schema", "Recovery-Partition"}, NormReferences: []string{"IEC 62443 — Ausfallsicherheit", "EU CRA — Verfuegbarkeit"}}, + + // ── Wartungsgefaehrdung (maintenance_hazard) ───────────────────── + {ID: "M210", ReductionType: "protection", SubType: "procedural", Name: "Lockout/Tagout-Verfahren (LOTO)", Description: "Vor Wartungsarbeiten werden alle Energiequellen gesperrt und gegen Wiedereinschalten gesichert. Entsperrung nur durch befugte Person.", HazardCategory: "mechanical", Examples: []string{"Vorhangschloss am Hauptschalter", "LOTO-Station mit persoenlichen Schloessern"}, NormReferences: []string{"ISO 14118 — Energietrennung", "TRBS 1112 — Instandhaltung"}}, + {ID: "M211", ReductionType: "information", SubType: "documentation", Name: "Wartungsanleitung mit Sicherheitshinweisen", Description: "Detaillierte Wartungsanleitung die alle sicherheitsrelevanten Schritte, erforderliche PSA und Restgefahren dokumentiert.", HazardCategory: "mechanical", Examples: []string{"Wartungshandbuch mit Schritt-fuer-Schritt-Anleitung", "Warnhinweise bei Restenergie"}, NormReferences: []string{"ISO 12100 — Benutzerinformation", "EN IEC 82079-1 — Gebrauchsanleitungen"}}, + {ID: "M212", ReductionType: "protection", SubType: "procedural", Name: "Freigabeverfahren nach Wartung", Description: "Nach Wartungsarbeiten wird ein dokumentierter Freigabeprozess durchlaufen bevor die Maschine wieder in Betrieb genommen wird.", HazardCategory: "mechanical", Examples: []string{"Checkliste Inbetriebnahme nach Wartung", "Funktionspruefung Schutzeinrichtungen"}, NormReferences: []string{"BetrSichV — Pruefpflichten", "TRBS 1201 — Pruefungen"}}, + + // ── Sensor-Fehler (sensor_fault) ───────────────────────────────── + {ID: "M213", ReductionType: "design", SubType: "control_design", Name: "Sensor-Redundanz fuer Sicherheitsfunktionen", Description: "Sicherheitsrelevante Sensoren werden redundant (2-kanalig) ausgefuehrt. Diskrepanz zwischen Kanaelen fuehrt zum sicheren Zustand.", HazardCategory: "software_control", Examples: []string{"Doppelter Positionssensor", "Redundante Druckmessung"}, NormReferences: []string{"ISO 13849-1 — Kategorie 3/4", "IEC 62061 — SIL-Architektur"}}, + {ID: "M214", ReductionType: "design", SubType: "control_design", Name: "Plausibilitaetspruefung Sensordaten", Description: "Sensordaten werden auf physikalische Plausibilitaet und zulaessige Aenderungsraten geprueft. Unplausible Werte loesen Sicherheitsreaktion aus.", HazardCategory: "software_control", Examples: []string{"Bereichsueberwachung Temperatur", "Gradientenueberwachung Drehzahl"}, NormReferences: []string{"ISO 13849-1 — Fehlererkennung", "IEC 61508 — Diagnostik"}}, + + // ── Betriebsarten-Verwechslung (mode_confusion) ───────────────── + {ID: "M215", ReductionType: "design", SubType: "control_design", Name: "Eindeutige Betriebsartenanzeige", Description: "Die aktive Betriebsart wird permanent und eindeutig am Bedienpult und auf dem HMI angezeigt. Keine Verwechslungsgefahr zwischen Modi.", HazardCategory: "software_control", Examples: []string{"LED-Anzeige je Betriebsart", "Farbcodierung auf HMI-Bildschirm"}, NormReferences: []string{"IEC 60204-1 — Betriebsartenwahl", "ISO 12100 — Betriebsarten"}}, + {ID: "M216", ReductionType: "design", SubType: "control_design", Name: "Zustandsbestaetigung bei kritischem Moduswechsel", Description: "Wechsel in sicherheitskritische Betriebsarten erfordert eine bewusste Zwei-Schritt-Bestaetigung des Bedieners.", HazardCategory: "software_control", Examples: []string{"Schluesselschalter + Quittierung", "Hold-to-Run im Einrichtbetrieb"}, NormReferences: []string{"ISO 12100 — Betriebsarten", "IEC 60204-1 — Zustimmungsschalter"}}, } } diff --git a/ai-compliance-sdk/internal/iace/pattern_engine.go b/ai-compliance-sdk/internal/iace/pattern_engine.go index 4e555bc..df121a3 100644 --- a/ai-compliance-sdk/internal/iace/pattern_engine.go +++ b/ai-compliance-sdk/internal/iace/pattern_engine.go @@ -19,12 +19,21 @@ type MatchOutput struct { ResolvedTags []string `json:"resolved_tags"` } +// MatchReason explains why a specific check passed or was relevant for a pattern match. +type MatchReason struct { + Type string `json:"type"` // "required_component_tag", "required_energy_tag", "lifecycle_match", "no_exclusion" + Tag string `json:"tag"` + Met bool `json:"met"` +} + // PatternMatch records which pattern fired and why. type PatternMatch struct { PatternID string `json:"pattern_id"` PatternName string `json:"pattern_name"` Priority int `json:"priority"` MatchedTags []string `json:"matched_tags"` + // Explainability: structured reasons why this pattern fired + MatchReasons []MatchReason `json:"match_reasons,omitempty"` // Detail fields from the pattern definition ScenarioDE string `json:"scenario_de,omitempty"` TriggerDE string `json:"trigger_de,omitempty"` @@ -136,16 +145,34 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { continue } - // Collect the tags that contributed to this match + // Collect the tags that contributed + build explainability reasons var matchedTags []string + var reasons []MatchReason for _, t := range p.RequiredComponentTags { if tagSet[t] { matchedTags = append(matchedTags, t) + reasons = append(reasons, MatchReason{Type: "required_component_tag", Tag: t, Met: true}) } } for _, t := range p.RequiredEnergyTags { if tagSet[t] { matchedTags = append(matchedTags, t) + reasons = append(reasons, MatchReason{Type: "required_energy_tag", Tag: t, Met: true}) + } + } + for _, t := range p.ExcludedComponentTags { + reasons = append(reasons, MatchReason{Type: "no_exclusion", Tag: t, Met: !tagSet[t]}) + } + if len(p.RequiredLifecycles) > 0 { + for _, lc := range p.RequiredLifecycles { + found := false + for _, ilc := range input.LifecyclePhases { + if ilc == lc { + found = true + break + } + } + reasons = append(reasons, MatchReason{Type: "lifecycle_match", Tag: lc, Met: found}) } } @@ -154,6 +181,7 @@ func (e *PatternEngine) Match(input MatchInput) *MatchOutput { PatternName: p.NameDE, Priority: p.Priority, MatchedTags: matchedTags, + MatchReasons: reasons, ScenarioDE: p.ScenarioDE, TriggerDE: p.TriggerDE, HarmDE: p.HarmDE, diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_client.go b/ai-compliance-sdk/internal/ucca/legal_rag_client.go index 5c7cd21..e7ccd74 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag_client.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag_client.go @@ -148,5 +148,13 @@ func (c *LegalRAGClient) ListAvailableRegulations() []CERegulationInfo { {ID: "enisa_ics_scada_dependencies", NameDE: "ENISA ICS/SCADA Abhaengigkeiten", NameEN: "ENISA ICS/SCADA Communication Dependencies", Short: "ENISA ICS/SCADA", Category: "guidance"}, {ID: "cisa_secure_by_design", NameDE: "CISA Secure by Design", NameEN: "CISA Secure by Design", Short: "CISA SbD", Category: "guidance"}, {ID: "enisa_cybersecurity_state_2024", NameDE: "ENISA State of Cybersecurity 2024", NameEN: "ENISA State of Cybersecurity in the Union 2024", Short: "ENISA 2024", Category: "guidance"}, + // BAuA — Technische Regeln (gemeinfrei, §5 UrhG) + {ID: "trbs", NameDE: "TRBS — Technische Regeln fuer Betriebssicherheit", NameEN: "TRBS — Technical Rules for Operational Safety", Short: "TRBS", Category: "trbs"}, + {ID: "trgs", NameDE: "TRGS — Technische Regeln fuer Gefahrstoffe", NameEN: "TRGS — Technical Rules for Hazardous Substances", Short: "TRGS", Category: "trgs"}, + {ID: "asr", NameDE: "ASR — Arbeitsstaettenregeln", NameEN: "ASR — Workplace Rules", Short: "ASR", Category: "asr"}, + // OSHA + {ID: "osha_1910", NameDE: "OSHA 1910 Subpart O — Maschinenschutz", NameEN: "OSHA 1910 Subpart O — Machinery and Machine Guarding", Short: "OSHA 1910", Category: "osha"}, + // EuGH + {ID: "eugh_c_588_21", NameDE: "EuGH C-588/21 P — Datenschutz-Urteil", NameEN: "ECJ C-588/21 P — Data Protection Judgment", Short: "EuGH C-588/21", Category: "eu_recht"}, } } diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go b/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go index b8da3df..8db10a3 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag_scroll.go @@ -105,6 +105,90 @@ func (c *LegalRAGClient) ScrollChunks(ctx context.Context, collection string, of return chunks, nextOffset, nil } +// ScrollDocumentIndex scrolls through all chunks in a collection using minimal +// payload (no text/vectors) and returns a deduplicated list of documents. +func (c *LegalRAGClient) ScrollDocumentIndex(ctx context.Context, collection string) ([]CEDocumentInfo, error) { + includeFields := []string{"regulation_id", "regulation_name_de", "regulation_name_en", "category", "source", "source_org"} + + // regulation_id → aggregated info + docMap := make(map[string]*CEDocumentInfo) + var offset interface{} + batchLimit := 500 + + for { + reqBody := map[string]interface{}{ + "limit": batchLimit, + "with_payload": map[string]interface{}{"include": includeFields}, + "with_vectors": false, + } + if offset != nil { + reqBody["offset"] = offset + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal scroll request: %w", err) + } + + url := fmt.Sprintf("%s/collections/%s/points/scroll", c.qdrantURL, collection) + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create scroll request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + if c.qdrantAPIKey != "" { + req.Header.Set("api-key", c.qdrantAPIKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("scroll request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("qdrant returned %d: %s", resp.StatusCode, string(body)) + } + + var scrollResp qdrantScrollResponse + if err := json.NewDecoder(resp.Body).Decode(&scrollResp); err != nil { + return nil, fmt.Errorf("failed to decode scroll response: %w", err) + } + + for _, pt := range scrollResp.Result.Points { + regID := getString(pt.Payload, "regulation_id") + if regID == "" { + continue + } + if existing, ok := docMap[regID]; ok { + existing.ChunkCount++ + continue + } + docMap[regID] = &CEDocumentInfo{ + RegulationID: regID, + NameDE: getString(pt.Payload, "regulation_name_de"), + NameEN: getString(pt.Payload, "regulation_name_en"), + Category: getString(pt.Payload, "category"), + SourceURL: getString(pt.Payload, "source"), + SourceOrg: getString(pt.Payload, "source_org"), + ChunkCount: 1, + } + } + + if scrollResp.Result.NextPageOffset == nil { + break + } + offset = scrollResp.Result.NextPageOffset + } + + docs := make([]CEDocumentInfo, 0, len(docMap)) + for _, d := range docMap { + docs = append(docs, *d) + } + return docs, nil +} + // Helper functions func getString(m map[string]interface{}, key string) string { diff --git a/ai-compliance-sdk/internal/ucca/legal_rag_types.go b/ai-compliance-sdk/internal/ucca/legal_rag_types.go index 743d3cf..ac8ffff 100644 --- a/ai-compliance-sdk/internal/ucca/legal_rag_types.go +++ b/ai-compliance-sdk/internal/ucca/legal_rag_types.go @@ -47,6 +47,17 @@ type ScrollChunkResult struct { SourceURL string `json:"source_url,omitempty"` } +// CEDocumentInfo represents a document in the CE corpus with metadata. +type CEDocumentInfo struct { + RegulationID string `json:"regulation_id"` + NameDE string `json:"name_de"` + NameEN string `json:"name_en"` + Category string `json:"category"` + SourceURL string `json:"source_url"` + SourceOrg string `json:"source_org"` + ChunkCount int `json:"chunk_count"` +} + // --- Internal Qdrant / Ollama HTTP types --- type ollamaEmbeddingRequest struct {