94233b7c66
Three coupled pieces of work, all landing the same PoC:
1. Backend gap-review endpoint (Task #7)
- internal/api/handlers/iace_handler_gap_review.go:
POST /projects/:id/llm-gap-review
feeds Limits-Form + current hazards + current mitigations to
the configured LLM (Qwen / Claude / OpenAI via ProviderRegistry),
parses a JSON suggestion list, filter+stamps confidence, falls
back to a static checklist when LLM is unavailable.
- Adopt step is NOT in this endpoint by design — the user clicks
Adopt in the frontend which calls the existing CreateHazard /
CreateMitigation handlers so provenance flows through the normal
audit trail.
2. Frontend modal + button (Task #8)
- app/sdk/iace/[projectId]/hazards/_components/LLMGapReviewModal.tsx:
reusable modal that POSTs the gap-review endpoint, renders
suggestions with Adopt/Reject UX, shows confidence + norm refs,
source-stamp llm_gap_review vs fallback_static.
- hazards/page.tsx: indigo "KI-Gap-Review" button next to the
existing "Eigene Gefaehrdung" button + modal mount.
3. Tech-File sources appendix (Task #29 — Stufe 4)
- internal/iace/document_export_sources.go: new pdfSourcesAppendix
method appended to ExportPDF. Groups cited norms by license rule
(R1 OSHA/EU-Recht / R3 BreakPilot patterns / R3 DIN-EN-ISO
identifier-only) and emits the legally required statement that
pauschal Impressum-Hinweise nicht ausreichen.
- extractCitedNorms() scans hazard/mitigation text for EN/ISO/IEC/
DIN identifiers in a narrow grammar so prose isn't turned into
spurious citations.
Bonus refactor:
- internal/app/routes.go reached the 500-LOC hard cap when the new
llm-gap-review route was added. Extracted registerIACERoutes into
routes_iace.go (136 LOC). Same wiring, no behaviour change.
Three of the four Attribution-Renderer stages (1, 2, 4) now produce
real output. Stufe 3 ships as <SourceBadge> + <LicenseModuleBanner>
already (commits dfac940 + b9e3eea earlier in this branch).
The PoC is intentionally conservative: every LLM-Suggestion stays
unverbindlich until a human clicks Adopt, and Adopt goes through the
existing normal CreateHazard/CreateMitigation flow (not yet wired in
this commit — separate iteration). The endpoint, modal and provenance
chain are in place for the next iteration to wire Adopt → write path.
135 lines
5.1 KiB
Go
135 lines
5.1 KiB
Go
package iace
|
|
|
|
// Sources & Licenses appendix for the IACE Tech-File PDF export.
|
|
// Stufe 4 of the Attribution Renderer (Task #29).
|
|
//
|
|
// The IACE engine generates hazards from BreakPilot Pattern-IDs that
|
|
// themselves cite ISO 12100, EN 13849, EN ISO 13855 etc. Those norm
|
|
// identifiers are R3 (DIN/EN copyright — identifier-only). The
|
|
// pattern-engine output itself is R3 (BreakPilot own work). OSHA values
|
|
// surfaced via the minimum-distance library are R1 (US Federal PD).
|
|
//
|
|
// This appendix aggregates what the Tech-File ACTUALLY cited and shows
|
|
// it grouped by license rule with the mandatory disclaimer that the
|
|
// per-export footer cannot be replaced by a pauschal Impressum-Hinweis.
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
// pdfSourcesAppendix renders the "Quellen & Lizenzen" appendix page.
|
|
// Called by ExportPDF after the regulatory classifications block.
|
|
func (e *DocumentExporter) pdfSourcesAppendix(pdf *gofpdf.Fpdf, hazards []Hazard, mitigations []Mitigation) {
|
|
pdf.SetFont("Helvetica", "B", 14)
|
|
pdf.SetTextColor(124, 58, 237)
|
|
pdf.CellFormat(0, 10, "Quellen und Lizenzen", "", 1, "L", false, 0, "")
|
|
pdf.Ln(2)
|
|
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
pdf.SetTextColor(80, 80, 80)
|
|
intro := "Diese Risikobeurteilung verwendet die deterministische BreakPilot IACE " +
|
|
"Pattern-Engine sowie zitierte Sicherheitsnormen. Die folgende Aufstellung " +
|
|
"listet die konkret in diesem Dokument zitierten Quellen mit ihrer Lizenzregel."
|
|
pdf.MultiCell(0, 5, intro, "", "L", false)
|
|
pdf.Ln(3)
|
|
|
|
pdf.SetFont("Helvetica", "B", 10)
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.CellFormat(0, 7, "R3 — BreakPilot Pattern-Engine (Eigenwerk, Identifier-Verweis)", "", 1, "L", false, 0, "")
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
pdf.SetTextColor(60, 60, 60)
|
|
pdf.MultiCell(0, 5,
|
|
"Alle in diesem Dokument referenzierten HP-XXXX-Identifier stammen aus der "+
|
|
"BreakPilot IACE Pattern-Library (Eigenwerk). Keine externe Lizenz-Attribution "+
|
|
"erforderlich.", "", "L", false)
|
|
pdf.Ln(3)
|
|
|
|
norms := extractCitedNorms(hazards, mitigations)
|
|
if len(norms) > 0 {
|
|
pdf.SetFont("Helvetica", "B", 10)
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.CellFormat(0, 7, "R3 — Sicherheitsnormen (DIN/EN/ISO/IEC, Identifier-Verweis)", "", 1, "L", false, 0, "")
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
pdf.SetTextColor(60, 60, 60)
|
|
pdf.MultiCell(0, 5,
|
|
"DIN-/EN-/ISO-/IEC-Normen unterliegen dem Urheberrecht der jeweiligen "+
|
|
"Normungsorganisation. In diesem Dokument werden Normen ausschliesslich "+
|
|
"als Identifier (Norm-Nummer und Abschnitt) zitiert; kein Volltext aus "+
|
|
"diesen Normen wurde reproduziert. Konkret zitiert:", "", "L", false)
|
|
pdf.Ln(1)
|
|
for _, n := range norms {
|
|
pdf.CellFormat(0, 5, " • "+n, "", 1, "L", false, 0, "")
|
|
}
|
|
pdf.Ln(2)
|
|
}
|
|
|
|
pdf.SetFont("Helvetica", "B", 10)
|
|
pdf.SetTextColor(0, 0, 0)
|
|
pdf.CellFormat(0, 7, "R1 — Hoheitsrecht / Public Domain (woertlich uebernehmbar)", "", 1, "L", false, 0, "")
|
|
pdf.SetFont("Helvetica", "", 9)
|
|
pdf.SetTextColor(60, 60, 60)
|
|
pdf.MultiCell(0, 5,
|
|
"Soweit Werte aus US Federal Code (OSHA 29 CFR Subpart O) oder EU-Recht "+
|
|
"(Maschinenverordnung 2023/1230, AI Act 2024/1689) referenziert werden, "+
|
|
"sind diese als R1 woertlich uebernehmbar. Keine Attribution-Pflicht.", "", "L", false)
|
|
pdf.Ln(4)
|
|
|
|
pdf.SetFont("Helvetica", "I", 8)
|
|
pdf.SetTextColor(120, 120, 120)
|
|
pdf.MultiCell(0, 4,
|
|
"Hinweis: Pauschalvermerke in AGB oder Impressum reichen rechtlich nicht — "+
|
|
"die werknahe Attribution erfolgt durch diese Quellenseite. Vollstaendiges "+
|
|
"Quellenverzeichnis aller im BreakPilot-System verwendeten Quellen siehe "+
|
|
"/sdk/licenses im Web-Frontend.", "", "L", false)
|
|
}
|
|
|
|
// extractCitedNorms scans hazard descriptions + scenario fields for
|
|
// recognised norm identifiers. The detection is intentionally narrow:
|
|
// only well-known prefixes (EN/ISO/IEC/DIN) and only when followed by
|
|
// digits, so free-form prose is not turned into spurious citations.
|
|
func extractCitedNorms(hz []Hazard, mt []Mitigation) []string {
|
|
seen := make(map[string]bool)
|
|
consider := func(s string) {
|
|
fields := strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == ' ' || r == ',' || r == ';' || r == '\n' || r == ';' || r == '('
|
|
})
|
|
for i := 0; i < len(fields)-1; i++ {
|
|
head := strings.ToUpper(strings.TrimSpace(fields[i]))
|
|
next := strings.TrimSpace(fields[i+1])
|
|
if !(head == "EN" || head == "ISO" || head == "IEC" || head == "DIN") {
|
|
continue
|
|
}
|
|
if next == "" {
|
|
continue
|
|
}
|
|
// Accept "ISO 12100", "EN 13849-1", "DIN EN 60204-1" etc.
|
|
if next[0] >= '0' && next[0] <= '9' {
|
|
seen[head+" "+next] = true
|
|
} else if head == "DIN" && (strings.HasPrefix(strings.ToUpper(next), "EN") || strings.HasPrefix(strings.ToUpper(next), "ISO")) && i+2 < len(fields) {
|
|
third := strings.TrimSpace(fields[i+2])
|
|
if third != "" && third[0] >= '0' && third[0] <= '9' {
|
|
seen[head+" "+next+" "+third] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, h := range hz {
|
|
consider(h.Description)
|
|
consider(h.Scenario)
|
|
consider(h.PossibleHarm)
|
|
}
|
|
for _, m := range mt {
|
|
consider(m.Description)
|
|
consider(m.Name)
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for k := range seen {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|